Android OOM案例分析

koaatuzl 7年前發布 | 11K 次閱讀 OOM 安卓開發 Android開發 移動開發

在Android(Java)開發中,基本都會遇到 java.lang.OutOfMemoryError (本文簡稱OOM),這種錯誤解決起來相對于一般的Exception或者Error都要難一些,主要是由于錯誤產生的root cause不是很顯而易見。由于沒有辦法能夠直接拿到用戶的內存dump文件,如果錯誤發生在線上的版本,分析起來就會更加困難。本文從一個具體的案例切入,介紹OOM分析的思路及相關工具的使用。

案例背景

在美團App 7.4~7.7版本期間,美食業務的OOM數量居高不下,遠高于歷史水平,主要都是DECODE本地的資源出錯。

圖中OOM數量為各版本發版后第一個月的統計量,包含新發版本及歷史版本。對比了同時期其他業務的情況,也有類似OOM。由于美食業務的訪問量占美團App的比重較大,因此,OOM的數量相對其他業務也多一些。

思路方案

在問題較為嚴重的7.6~7.7版本期間,團隊對OOM頻現的原因有過各種猜測。筆者懷疑過是否是業務上某些修改引起的,例如頭圖尺寸變大,或者是由頁面模塊加載方式引起的等等。但這些與OOM問題出現的時間并不吻合。其次也懷疑過是否由某些ROM的Bug導致,但此推斷缺乏有力的證據支撐。因此,要找到OOM的root cause,根本途徑還是找到誰占的內存最多,然后再根據具體case具體分析,為什么占了這么多。

采集用戶手機內存信息

要分析內存的占用,需要內存的dump文件,但是dump文件一般都比較大,讓用戶配合上傳dump文件不合適。所以希望能夠運行時采集一些內存的特征然后隨著crash日志上報上來。當用戶發生OOM時,dump出用戶的內存,然后基于 com.squareup.haha:haha:2.0.3 分析,得到一些關鍵數據(內存占用最多的實例及所占比例等)。但這個方案很快就被證明是不可行的。主要基于下面幾個原因:

  • 需要引入新的庫。
  • dump和分析內存都很耗時,效率難以接受。
  • OOM時內存已經幾乎耗盡,再加載內存dump文件并分析會導致二次OOM,得不償失。

模擬復現OOM

采集用戶手機內存信息的方案不可行,那么只能采取復現用戶場景的方式。由于發生OOM時,用戶操作路徑的不確定性,無法精確復現線上的OOM,因此采取模擬復現的方式,最終發生OOM時的棧信息基本一致即可。為了能夠盡量模擬用戶發生OOM的場景,需要基本條件基本一致,即用戶使用的手機的各種相關參數。

挖掘OOM特征

分析7.4以來的OOM,列出發生OOM的機器的特征,主要是內存和分辨率,適當考慮其它因素例如系統版本。

機型 內存 分辨率 OS stack log
OPPO N1(T/W) 2G 1920*1080 4.2.2 java.lang.OutOfMemoryError
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
HM 2LTE-CMCC 1G 1280*720 4.4.4 java.lang.OutOfMemoryError
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
Newman CM810 2G 1920*1080 4.4.4 java.lang.OutOfMemoryError
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
LGL22 2G 1830*1080 4.2.2 java.lang.OutOfMemoryError
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
OPPO X909 2G 1920*1080 4.2.2 java.lang.OutOfMemoryError
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
Lenovo K900 2G 1920*1080 4.2.2 java.lang.OutOfMemoryError
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
GiONEE E6 2G 1920*1080 4.2.1 java.lang.OutOfMemoryError
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)

這些特征可以總結為:內存一般,分辨率偏高,OOM的堆棧log基本一致。其中,OPPO N1(T/W)上所發生的OOM比重較高,約為65%,因此選定這款機器作為復現OOM的機器。

關鍵數據(內存dump文件)

需要復現OOM然后獲取內存dump。思路是采取內存壓力測試,讓問題暴露的快速且充分。具體方案為:

  • 選取圖片資源多且較為復雜的頁面,比如美食的POI詳情頁。
  • 加載30次該頁面,為了增加OOM的幾率,30個POI頁面的ID是不同的。

OOM發生后,使用Android Studio自帶的Android Monitor dump出HPROF文件,然后使用SDK中的hprof-conv(位于sdk_root/platform-tools)工具轉換為標準的Java堆轉儲文件格式,這樣可以使用MAT(Eclipse Memory Analyzer)繼續分析。

切到histogram視圖,按shadow heap降序排列。

選取byte數組,右擊->list objects->with incoming references,降序排列可以看到有很多大小一致的byte[]實例。

右擊其中一個數組->Path to GC Roots-> exclude xxx references

如上圖所示,這些byte[]都是系統的EdgeEffect的drawable所持有,drawable對應的bitmap占用的空間為1566 * 406 * 4 = 2543184,與byte數組的大小一致。

再看另外一個:

這些byte[]是被App的一個背景圖所持有,如下圖:

通過ImageView的ID(如圖)及build目錄下的R.txt反查可知該ImageView的ID名稱,即可知其設置的背景圖的大小為720 * 200(xhdpi),加載到內存并考慮density,size剛好是1080 * 300 * 4 = 1296000,與byte數組大小一致。

數據分析

為什么會出現這些大小一致的byte數組,或者說,為什么會創建多份EdgeEffect的drawable?查看EdgeEffect的源碼(4.2.2)可知,其drawable成員也是通過 Resources.getDrawable 系統調用獲取的。

/**
 * Construct a new EdgeEffect with a theme appropriate for the provided context.
 * @param context Context used to provide theming and resource information for the EdgeEffect
 */
public EdgeEffect(Context context) {
    final Resources res = context.getResources();
    mEdge = res.getDrawable(R.drawable.overscroll_edge);
    mGlow = res.getDrawable(R.drawable.overscroll_glow);

        ******

    mMinWidth = (int) (res.getDisplayMetrics().density * MIN_WIDTH + 0.5f);
    mInterpolator = new DecelerateInterpolator();
}

ImageView(View)獲取background對應的drawable的過程類似。

for (int i = 0; i < N; i++) {
    int attr = a.getIndex(i);
    switch (attr) {
        case com.android.internal.R.styleable.View_background:
            background = a.getDrawable(attr); // TypedArray.getDrawable
            break;
        ******
    }
}

不論是Resources.getDrawable還是TypedArray.getDrawable,最終都會調用Resources.loadDrawable。繼續看 Resources.loadDrawable 的源碼,發現的確是使用了緩存。對于同一個drawable資源,系統只會加載一次,之后都會從緩存去取。

既然drawable的加載機制并沒有問題,那么drawable所在的緩存實例或者獲取drawable的Resources實例是否是同一個呢?通過下面的代碼,打印出每個Activity的Resources實例及Resources實例的drawable cache。

//noinspection unchecked
LongSparseArray<WeakReference<Drawable.ConstantState>> cache = (LongSparseArray<WeakReference<Drawable.ConstantState>>) Hack.into(Resources.class).field("mDrawableCache").get(getResources());
Object appCache = Hack.into(Resources.class).field("mDrawableCache").get(getApplication().getResources());
Log.e("oom", "Resources: {application=" + getApplication().getResources() + ", activity=" + getResources() + "}");
Log.e("oom", "Resources.mDrawableCache: {application=" + appCache + ", activity=" + cache + "}");

這也進一步解釋了另外一個現象,即這些大小相同的數組的個數基本和啟動Activity的數量成正比。

通過數據分析可知,這些drawable之所以存在多份,是因為其所在的Resources實例并不是同一個。進一步debug可知,Resources實例存在多個的原因是開啟了標志位 sCompatVectorFromResourcesEnabled

雖然最終造成OOM突然增多的原因只是開啟一個標志位,但是這也告誡大家閱讀API文檔的重要性,其實很多時候API的使用說明已經明確告知了使用的限制條件甚至風險。

7.8版本關閉了此標志,發版后第一個月的OOM數量(包含歷史版本)為153,如下圖。

其中新版本發生的OOM數量為22。

總結

對于線上出現的OOM,如何分析和解決可以大致分為三個步驟:

  1. 充分挖掘特征。在挖掘特征時,需要多方面考慮,此過程更多的是猜測懷疑,所以可能的方面都要考慮到,包括但不限于代碼改動、機器特征、時間特征等,必要時還需要做一定的統計分析。
  2. 根據掌握的特征尋找穩定的復現的途徑。一般需要做內存壓力測試,這樣比較容易達到OOM的臨界值,只是簡單的一些正常操作難以觸發OOM。
  3. 獲取可分析的數據(內存dump文件)。利用MAT分析dump文件,MAT可以方便的按照大小排序實例,可以查看某些實例到GC ROOT的路徑。

 

 

來自:http://tech.meituan.com/oom_analysis.html

 

 本文由用戶 koaatuzl 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!