Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲

OOM詳解

編輯:關於Android編程

OOM主要有兩種原因導致:
        1. 加載大圖片;
        2. 內存洩漏;
        因此這篇文章會簡單介紹一下這兩個方面,希望對大家解決OOM問題有所幫助。
一、加載大圖片
在Android應用中加載Bitmap的操作是需要特別小心處理的,因為Bitmap會消耗很多內存。比如,Galaxy Nexus的照相機能夠拍攝2592x1936 pixels (5 MB)的圖片。 如果bitmap的圖像配置是使用ARGB_8888 (從Android 2.3開始的默認配置) ,那麼加載這張照片到內存大約需要19MB(2592*1936*4 bytes) 的空間,從而迅速消耗掉該應用的剩余內存空間。
對於分辨率比我們手機屏幕的分辨率高得多的圖片,我們應該加載一個縮小版本的圖片,從而避免超出程序的內存限制。下面我們就來看一看,如何對一張大圖片進行適當的壓縮,讓它能夠以最佳大小顯示的同時,還能防止OOM的出現。
BitmapFactory這個類提供了多個解析方法(decodeByteArray, decodeFile, decodeResource等)用於創建Bitmap對象,我們應該根據圖片的來源選擇合適的方法。比如SD卡中的圖片可以使用decodeFile方法,網絡上的圖片可以使用decodeStream方法,資源文件中的圖片可以使用decodeResource方法。這些方法會嘗試為已經構建的bitmap分配內存,這時就會很容易導致OOM出現。為此每一種解析方法都提供了一個可選的BitmapFactory.Options參數,將這個參數的inJustDecodeBounds屬性設置為true就可以讓解析方法禁止為bitmap分配內存,返回值也不再是一個Bitmap對象,而是null。雖然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType屬性都會被賦值。這個技巧讓我們可以在加載圖片之前就獲取到圖片的長寬值和MIME類型,從而根據情況對圖片進行壓縮。如下代碼所示:

BitmapFactory.Options options = new BitmapFactory.Options();  
     options.inJustDecodeBounds = true; 
     BitmapFactory.decodeResource(getResources(), R.id.myimage, options); 
     int imageHeight = options.outHeight; 
     int imageWidth = options.outWidth; 
     String imageType = options.outMimeType;

為了避免java.lang.OutOfMemory 的異常,我們需要在真正解析圖片之前檢查它的尺寸(除非你能確定這個數據源提供了准確無誤的圖片且不會導致占用過多的內存)。
通過上面的步驟我們已經獲取到了圖片的尺寸,這些數據可以用來幫助我們決定應該加載整個圖片到內存中還是加載一個縮小的版本。有下面一些因素需要考慮:

預估一下加載整張圖片所需占用的內存。 為了加載這一張圖片你所願意提供多少內存。 用於展示這張圖片的控件的實際大小。 當前設備的屏幕尺寸和分辨率。
例如,如果把一個大小為1024x768像素的圖片顯示到大小為128x96像素的ImageView上,就沒有必要把整張原圖都加載到內存中。
那我們怎樣才能對圖片進行壓縮呢?通過設置BitmapFactory.Options中inSampleSize的值就可以實現。比如我們有一張2048*1536像素的圖片,將inSampleSize的值設置為4,就可以把這張圖片壓縮成512*384像素。原本加載這張圖片需要占用13M的內存,壓縮後就只需要占用0.75M了(假設圖片是ARGB_8888類型,即每個像素點占用4個字節)。下面的方法可以根據傳入的寬和高,計算出合適的inSampleSize值:
public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

注意: inSampleSize的取值應該總是為2的指數,比如1、2、4、8、16,等等。如果外界傳遞給系統的inSampleSize不為2的指數,那麼系統會向下取整並選擇一個最接近2的指數來代替,比如3,系統會選擇2來代替,但是這個結論並非在所有的Android版本上都能成立。
使用這個方法,首先你要將BitmapFactory.Options的inJustDecodeBounds屬性設置為true,解析一次圖片。然後將BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中,就可以得到合適的inSampleSize值了。之後再解析一次圖片,使用新獲取到的inSampleSize值,並把inJustDecodeBounds設置為false,就可以得到壓縮後的圖片了。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                     int reqWidth, int reqHeight) {
    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

下面的代碼非常簡單地將任意一張圖片壓縮成100*100的縮略圖,並在ImageView上展示。

mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

當然我們也可以通過替換合適的BitmapFactory.decode* 方法來實現一個類似的方法,從其他的數據源解析Bitmap。

二、內存洩漏(Memory leak)
1.Service導致內存洩漏:Service執行完沒有停止操作或者停止失敗就會導致內存洩漏,針對這種情況官方推薦使用IntentService,IntentService的最大特點就是後台任務執行結束後會自動停止。
2.使用依賴注入框架導致內存洩漏:依賴注入框架雖然簡化了復雜的編碼操作,但為了要搜尋代碼中的注解,通常都需要經歷較長的初始化過程,並且還可能將一些你用不到的對象也一並加載到內存當中。這些用不到的對象會一直占用著內存空間,可能要過很久之後才會得到釋放。
3.靜態對象導致內存洩漏:靜態變量的生命周期和類是息息相關的,它們分配在方法區上,垃圾回收一般不會回收這一塊的內存。所以我們在代碼中用到靜態對象,在不用的時候如果不賦null值消除對象的引用的話,那麼這些對象是很難被垃圾回收的。如果這些對象一多或者比較大的話,程序出現OOM的概率就比較大了。因為靜態變量而出現內存洩漏是很常見的。
4.Context導致內存洩漏: android 中很多地方都要用到context,連基本的Activty 和 Service都是從Context派生出來的,我們利用Context主要用來加載資源或者初始化組件,在Activity中有些地方需要用到Context的時候,我們經常會把context給傳遞過去了,將context傳遞出去就有可能延長了context的生命周期,最終導致了內存洩漏。例如 我們將activty context對象傳遞給一個後台線程去執行某些操作,如果在這個過程中因為屏幕旋轉而導致activity重建,那麼原先的activity對象不會被回收,因為它還被後台線程引用著,如果這個activity消耗了比較多的內存,那麼新建activity或者後續操作可能因為舊的activity沒有被回收而導致內存洩漏。所以,遇到需要用到context的時候,我們要合理選擇不同的context,對於android應用來說還有一個單例的Application Context對象,該對象生命周期和應用的生命周期是綁定的。選擇context應該考慮到它的生命周期,如果使用該context的組件的生命周期超過該context對象,那麼我們就要考慮是否可以用application context。如果真的需要用到該context對象,可以考慮用弱引用來WeakReference來避免內存洩漏。
5.非靜態內部類導致的內存洩漏:非靜態的內部類會持有外部類的一個引用,所以和前面context說到的一樣,如果該內部類生命周期超過外部類的生命周期,就可能引起內存洩露了,如AsyncTask和Handler。因為在Activity中我們可能會用到匿名內部類,所以要小心管理其生命周期。 如果明確生命周期較外部類長的話,那麼應該使用靜態內部類。
6.Drawable對象的回調隱含的內存洩漏:當我們為某一個view設置背景的時候,view會在drawable對象上注冊一個回調,所以drawable對象就擁有了該view的引用了,進而對整個context都有了間接的引用了,如果該drawable對象沒有管理好,例如設置為靜態,那麼就會導致內存洩漏。
7.監聽器注冊沒注銷造成內存洩漏:在Android程序裡面存在很多需要register與unregister的監聽器,我們需要確保在合適的時候及時unregister那些監聽器。自己手動add的listener,需要記得及時remove這個listener。
8.資源對象沒關閉造成內存洩漏:資源性對象比如(Cursor,File文件等)往往都用了一些緩沖,我們在不使用的時候,應該及時關閉它們,以便它們的緩沖及時回收內存。它們的緩沖不僅存在於java虛擬機內,還存在於java虛擬機外。如果我們僅僅是把它的引用設置為null,而不關閉它們,往往會造成內存洩露。因為有些資源性對象,比如SQLiteCursor(在析構函數finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。 因此對於資源性對象在不使用的時候,應該調用它的close()函數,將其關閉掉,然後才置為null.在我們的程序退出時一定要確保我們的資源性對象已經關閉。
9.單例對象中不合理的持有造成內存洩漏:雖然單例模式簡單實用,提供了很多便利性,但是因為單例的生命周期和應用保持一致,使用不合理很容易出現持有對象的洩漏。
10.集合容器中的對象洩漏:當集合裡面的對象屬性被修改後,再調用remove()方法時不起作用。

Set set = new HashSet();
Person p1 = new Person("唐僧","pwd1",25);
set.add(p1);
p1.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發生改變
set.remove(p1); //此時remove不掉,造成內存洩漏 

11.Webview導致內存洩漏:Android中的WebView存在很大的兼容性問題,不僅僅是Android系統版本的不同對WebView產生很大的差異,另外不同的廠商出貨的ROM裡面WebView也存在著很大的差異。更嚴重的是標准的WebView存在內存洩露的問題,看這裡WebView causes memory leak - leaks the parent Activity。所以通常根治這個問題的辦法是為WebView開啟另外一個進程,通過AIDL與主進程進行通信,WebView所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。
12.屬性動畫導致內存洩露:從Android3.0開始,Google提供了屬性動畫,屬性動畫中有一類無限循環的動畫,如果在Activity中播放此類動畫且沒有在onDestroy中去停止動畫,那麼動畫就會一直播放下去,盡管已經無法在界面上看到動畫效果了,並且這個時候Activity的View會被動畫持有,而View又持有了Activity,最終Activity無法釋放。解決方法是在Activity的onDestroy中調用animator.cancle()來停止動畫。

參考:
1.官方文檔:高效顯示Bitmap
2.Android OOM 問題的總結
3.Android內存優化之OOM
4.Android內存洩漏簡介
5.Java內存洩漏詳解
6.Android WebView 內存洩漏

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved