Android OOM 排查與解決——圖片載入優化

weixin_33782386發表於2017-11-03

1、OOM 引起與表現

        在 Android 這種移動裝置上,如果程式碼沒有處理好,很容易引發記憶體持續佔用與洩漏,導致 OOM(OutOfMemoryError) 異常,進而導致 App 程式 Crash 掛掉。

        在 Android 開發中,一個典型的 OOM 異常如下:

        
1362764-9eada15af7d02d81
OOM 異常

        一旦碰上了這類錯誤,我們往往需要去排查記憶體了。導致 OOM 的一些情況比較常見,大多數情況下,大家可能遇到的都是同一種情況:

  • Activity 洩漏導致;
  • 層次龐大複雜的 View 檢視導致;
  • 大量圖片持續佔用導致;
  • 其他資源持續未釋放導致。

2、Android Studio 檢視記憶體佔用

        在 Android Studio 裡面,我們可以在 Monitors 視窗中,實時對 App 記憶體進行監控,我們可以看出 App 的 HeapSize已經使用的記憶體大小剩餘記憶體大小以及峰值變化。有了這些資訊,我們可以在某個頁面開啟和關閉時進行監控,從而對比該頁面佔用記憶體變化,可以很方便的定位問題。

        
1362764-069158303610cffe
Monitors 檢視記憶體佔用

        在這張圖中,如果已使用的記憶體大小(Allocated)接近到 HeapSize 的大小,App 將會處於非常危險的狀態中,很有可能下一個操作就會直接導致 OOM,通過 Android Studio,我們可以防患於未然,在 Debug 階段進行預防。

3、adb 檢視記憶體佔用

        adb 工具也是一個非常有用的工具,我們可以通過它來檢視 App 記憶體佔用。

3.1、檢視 JVM 的 HeapSize 等引數

        通過命令 adb shell getprop dalvik.vm.heapsize 可以直接檢視 Dalvik 虛擬機器為 App 規定的最大 HeapSize

        
1362764-4f55802f62e8bf6b
getprop dalvik.vm.heapsize

        一般來說,App 可達到的最大 HeapSize 為 dalvik.vm.heapgrowthlimit 所規定的大小。但是如果我們在 AndroidManifest.xml 中為 Application 新增 android:largeHeap="true" 屬性,App 可達到的最大 HeapSize 則被調整為 dalvik.vm.heapsize 規定的值。

        雖然新增 android:largeHeap="true" 屬性將大大降低 OOM 的概率,但除非萬不得已的情況下,否則不要使用該屬性。出現 OOM 後,我們首先應該排查整個 App,找出記憶體瓶頸予以解決。

3.2、檢視 App 記憶體佔用

        通過命令 adb shell dumpsys meminfo [package_name] 可以檢視 App 所佔用記憶體:

        
1362764-c449b27430bac284
dumpsys meminfo 檢視記憶體佔用

        通過這個命令,App 所佔資源情況一目瞭然,甚至我們可以看到整個 App 中 View 個數、Activity 個數——這對於排查 Activity 洩漏和優化 View 層級也是非常有幫助的。

4、圖片載入導致 OOM

        而在一個 App 中,圖片處理不恰當往往是 OOM 錯誤出現的元凶——因為 App 中所有圖片動輒佔用幾十 M 的記憶體。如果我們能優先著手排查這一塊,將會對 App 的記憶體優化帶來 最直接最明顯 的改觀。而圖片的不恰當處理操作一般有如下一些:

  • 直接載入 超大尺寸 圖片;
  • 圖片載入後 未及時釋放
  • 在頁面中,同時載入 非常多 的圖片;

4.1、超大尺寸圖片處理

        現在的手機攝像頭畫素比較高,攝製出來的照片尺寸非常大,比如在一款還算老舊的手機上面,拍攝的圖片尺寸竟然達到了 2368 x 4224!因為採用 jpeg 格式的緣故,這張圖片在磁碟上才1.9M,但如果我們不加任何處理,按原尺寸載入到記憶體中,佔用的記憶體將會非常可觀。

        所以,針對大圖的載入,比較常用的方法是進行 DownSampling(向下取樣),許多部落格或技術站點對該方案有詳細的描述,在此不再贅述,簡單原理用程式碼表述如下:

public static int calcInSampleSize(
        int width, int height, int requestWidth, int requestHeight) {

    int inSampleSize = 1;
    if (requestWidth <= 0 || requestHeight <= 0) {
        return inSampleSize;
    }

    if (width > requestWidth || height > requestHeight) {
        int widthRatio = Math.round((float) width / (float) requestWidth);
        int heightRatio = Math.round((float) height / (float) requestHeight);

        inSampleSize = Math.min(widthRatio, heightRatio);
    }

    return inSampleSize;
}

public static Bitmap decodeBitmapFromUri(
    Context context, Uri uri, int requestWidth, int requestHeight) {

    BitmapFactory.Options options = getResourceOptions(context, uri);
    options.inSampleSize = calcInSampleSize(
            options.outWidth, options.outHeight, requestWidth, requestHeight);
    options.inJustDecodeBounds = false;

    // ...
    
    ContentResolver resolver = context.getContentResolver();
    Bitmap bitmap = BitmapFactory.decodeStream(
            resolver.openInputStream(uri), new Rect(), options);
    // ...

    return bitmap;
}

        這樣,如果我們要載入一張圖片到 View 上,我們可以通過 view.getMeasuredWidth() 和 view.getMeasuredHeight() 得到 View 的寬和高,然後按這個大小進行取樣,得到的 Bitmap 將會是尺寸適合的圖片,不會佔用額外記憶體,圖片在 View 上展示出來質量也比較高。

4.2、及時釋放圖片

        一般不要靜態快取圖片,就算有快取,也可以結合 LRU 機制來保證快取圖片的個數和佔用記憶體。Android SDK 已經提供了 LruCache 類來實現 LRU 機制。

4.3、避免同時載入大量圖片

        避免同一時間載入大量的圖片,也可以為我們的記憶體優化提供不小的收益。比如,在一個 ScrollView 中有非常多的 ImageView,這時候,佔用的記憶體往往非常客觀,因為就算一些 View 我們在螢幕視野裡面看不到,它還是持續佔用記憶體。我們可以通過 RecyclerView 或者 ListView 來予以替換,從而達到記憶體優化的效果。

        在我的開發過程中,就遇到了這樣一個例子。一個頁面用 ScrollView 來佈局,裡面有 26 張左右的圖片,這時候,整個 App 的記憶體佔用長期達到了 90M 左右!一直徘徊在 OOM 邊緣。在我把這個頁面用 RecyclerView 替換掉 ScrollView 後,整個 App 記憶體竟然下降了 40M 之多!!!整個 App 變得非常順滑。

5、採用開源庫載入圖片

        現在已經有非常多的圖片載入庫供我們使用了,比較流行的有:FrescoUniversal-Image-LoaderPicassoVolley 等等。這些開源庫一般來說,對記憶體的優化已經比較全面了,比我們自己手工管理記憶體來的好。所以,可以根據專案的實際情況靈活選用。

        比如,我目前所使用的 Fresco 庫,就可以靈活設定圖片尺寸,避免載入大尺寸的圖片(setResizeOptions):

public static void displayImage(DraweeView draweeView, Uri uri) {
    Size size = getAppropriateSize(draweeView);
    
    ImageRequest request = ImageRequestBuilder
            .newBuilderWithSource(uri)
            .setResizeOptions(new ResizeOptions(size.mWidth, size.mHeight))
            .setAutoRotateEnabled(true)
            .build();
    
    DraweeController controller = Fresco.newDraweeControllerBuilder()
            .setUri(uri)
            .setImageRequest(request)
            .setOldController(draweeView.getController())
            .build();
            
    draweeView.setController(controller);
}

private static Size getAppropriateSize(View view) {
    int width = view.getMeasuredWidth();
    int height = view.getMeasuredHeight();

    if (width <= 0 || height <= 0) {
        width = view.getWidth();
        height = view.getHeight();
    }

    Size size = MiscUtils.getScreenSize();
    if (width <= 0 || height <= 0 || width > size.mWidth || height > size.mHeight) {
        width = size.mWidth;
        height = size.mHeight;
    }

    return new Size(width, height);
}

        當然,我們還要在 ImagePipelineConfig 中開啟 DownSamplingsetDownsampleEnabled(true)):


public static void initFresco(Context context) {
    ImagePipelineConfig config = ImagePipelineConfig
            .newBuilder(context)
            .setDownsampleEnabled(true)
            .build();
            
    Fresco.initialize(context, config);
}

相關文章