Android 記憶體洩露詳解

Snake_sss發表於2018-12-31

專案經驗,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)

前言

記憶體洩露說簡單也簡單,說複雜也複雜。簡單是因為我們有很多工具,比如 Android Studio Profiler、MAT 等,對記憶體洩露進行定位。複雜是因為我們需要了解很多其它知識,比如 Android 虛擬機器(Dalvik 或者 ART)的自動垃圾回收機制、Android 平臺應用記憶體佔用分析、Android Context 原理等,以看懂這些工具提供的資料。否則,無法進行下一步的記憶體洩露分析,或者只知其然,而不知其所以然。

所以,強烈推薦先閱讀筆者的前三篇文章後,再閱讀本文:

如果你已經對上述內容非常熟悉或有所瞭解,也可以直接閱讀本文。

至於 Android Context 原理,筆者會就 Android 9.0(Pie,API28)原始碼進行簡要分析。

如有錯誤,歡迎指正。

記憶體洩露概念

我們知道在 C 語言中,記憶體是需要開發人員自己管理的,使用 malloc() 分配記憶體,free() 釋放記憶體。如果沒有呼叫 free() 進行釋放,將導致記憶體洩露。而 Java 虛擬機器實現了自動記憶體管理機制,將開發人員從手動管理中解放出來。那麼為什麼也會存在“記憶體洩露”呢?其實,這裡的“記憶體洩露”指的是變數的存活時間超過了其本身的生命週期,說的糙一點,就是“該死的時候沒死”。在實體記憶體有限的移動裝置上,每個 Java 程式都有一個記憶體使用閾值,超過這個閾值時,虛擬機器將會丟擲 OutOfMemoryError。而持續的記憶體洩露,很可能會導致 OutOfMemoryError,這也是我們為什麼要修復記憶體洩露的原因。

單個應用堆記憶體上限

檢視 Android 裝置上單個應用的 Java 堆記憶體使用上限:

java 程式碼:

ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
Logger.debug("JavaHeap", "dalvik.vm.heapgrowthlimit: %dM", am.getMemoryClass());
Logger.debug("JavaHeap", "dalvik.vm.heapsize: %dM", am.getLargeMemoryClass());
複製程式碼

Logger 是筆者封裝的一個日誌列印工具類:Logger.java

shell 命令:

adb shell getprop dalvik.vm.heapgrowthlimit
adb shell getprop dalvik.vm.heapsize
複製程式碼

分別對應:

  • 普通應用 Java 堆使用上限:對應 /system/build.prop 中 "dalvik.vm.heapgrowthlimit"
  • 大應用 Java 堆使用上限:需要在 Manifest application 標籤中設定 android:largeHeap="true",對應 /system/build.prop 中 "dalvik.vm.heapsize"

示例(小米6,Android 8.0,MIUI 10 8.12.13):

2018-12-31 17:13:39.335 4142-4142/? D/WanAndroid: JavaHeap: dalvik.vm.heapgrowthlimit: 256M
2018-12-31 17:13:39.335 4142-4142/? D/WanAndroid: JavaHeap: dalvik.vm.heapsize: 512M
複製程式碼

檢視記憶體資訊

上一篇文章說過,我們可以使用:

adb shell dumpsys meminfo [-s|-d|-a] <package_name|pid>
複製程式碼

[] 表示命令選項,<> 表示命令引數。

選項 作用
-s 輸出概要記憶體資訊
-d 輸出詳細記憶體資訊
-a 輸出全部記憶體資訊

示例(小米6,Android 8.0,MIUI 10 8.12.13)

D:\Android\projects\wanandroid_java>adb shell dumpsys meminfo -s com.yuloran.wanandroid_java
Applications Memory Usage (in Kilobytes):
Uptime: 563701889 Realtime: 1391868153

** MEMINFO in pid 5897 [com.yuloran.wanandroid_java] **

 App Summary
                       Pss(KB)
                        ------
           Java Heap:     9384
         Native Heap:    15636
                Code:    28680
               Stack:      120
            Graphics:     5688
       Private Other:     8020
              System:     3788

               TOTAL:    71316       TOTAL SWAP PSS:       44

 Objects
               Views:        0         ViewRootImpl:        0
         AppContexts:        2           Activities:        0
              Assets:       18        AssetManagers:        2
       Local Binders:       24        Proxy Binders:       21
       Parcel memory:        8         Parcel count:       34
    Death Recipients:        3      OpenSSL Sockets:        0
            WebViews:        0
複製程式碼

以上顯示的是 PSS(不瞭解 PSS的,請閱讀筆者的上篇文章)資料,單位是 KB。應用來自筆者的 JetPack 實踐專案 wanandroid_java

此處我們主要關注 Activities,這是定位應用是否存在記憶體洩露的切入點。因為 Activity 作為應用與使用者的互動入口,絕大多數的記憶體洩露,最終都會反應到 Activity 上。正常情況下,開啟一個 Activity,Activities 計數+1,退出一個 Activity,Activities 計數-1,退出應用,Activities 計數=0。 如果不是,說明你的應用存在記憶體洩露。顯然,上圖所示的資料,Activities 為 0,不存在記憶體洩露。

至於 AppContexts,表示的是程式中仍然存活的 Context 物件,相較於 Activities 重要性不高。我們知道 Application、Activity、Service 都是 Context 物件,所以這些物件的數量,都會納入 AppContexts 計數。而 ContentProvider、BroadcastReceiver 的 Context 是外部傳進去的,所以不會對 AppContexts 的計數結果產生影響。一般情況下,Application 的數量為 1,除非應用開了多個程式(一個 Java 程式對應一個虛擬機器,所以自然也對應一個新 Application),筆者的應用並沒有使用多程式,那麼為什麼 AppContexts 數量為 2 呢?使用 Android Studio Profiler 除錯一下:

Android  記憶體洩露詳解

Instance View 視窗可以檢視該物件的內部引用,Reference 視窗可以檢視該物件被哪些外部物件引用。我們分別檢視下這兩個 Context 物件正被哪些物件引用:

Android  記憶體洩露詳解

如上圖所示,該物件被 MyApplication 的成員變數 mBase 引用,沒有問題。

Android  記憶體洩露詳解

如上圖所示,該物件被 ActivityThread 的成員變數 mSystemContext 引用,也沒有問題。

Context 原始碼簡析

既然上面提到了 Context,此處簡單分析下其子類 Application、Activity、Service 的建立原始碼。由於 Application 一般在建立 Activity 時順帶建立,所以直接從 Activity 的建立原始碼切入,原始碼基於 SDK Android 9.0(Pie,API28)。

Activity 建立原始碼

-> ActivityThread::performLaunchActivity()

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // 建立一個適用於 Activity 的 ContextImpl 物件,此處變數名改為 activityContext 更合適
        // ContextImpl extends Context,所以也是 Context 的子類
        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            // 反射建立 Activity 物件
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
        } catch (Exception e) {
        }

        try {
            // 建立 Application 物件
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
            if (activity != null) {
                // 將 activity 賦給 ContextImpl::mOuterContext 物件,可以看出 mOuterContext 代表的是實際元件物件,
                // 比如具體的 Application、Activity、Service 物件,此處為 Activity 物件
                appContext.setOuterContext(activity);
                // 將 ContextImpl 型別的 appContext 物件賦給 ContextWrapper::mBase 物件,而大多數時候,實際幹活的
                // 也正是這個物件
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
                // 設定主題
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }
                // 通過 Instrumentation 型別的 mInstrumentation 物件,間接呼叫 onCreate()
                // 注意 mInstrumentation 物件,這是代理模式的一個應用,用於託管直接建立系統元件或呼叫
                // 系統元件功能,用來監測和協助執行 Android 應用。
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
            }
        } catch (SuperNotCalledException e) {
        } catch (Exception e) {
        }
        return activity;
    }
複製程式碼

-> LoadedApk::makeApplication()

    public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
        Application app = null;
        try {
            java.lang.ClassLoader cl = getClassLoader();
            // 建立一個適用於 Application 的 ContextImpl 物件
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            // 建立 Application 物件
            app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);
            // 將 app 賦給 ContextImpl::mOuterContext 物件,可以看出 mOuterContext 代表的是實際元件物件,
            // 比如具體的 Application、Activity、Service 物件,此處為 Application 物件
            appContext.setOuterContext(app);
        } catch (Exception e) {
        }
        return app;
    }
複製程式碼

-> Instrumentation::newApplication()

    public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Application app = getFactory(context.getPackageName()).instantiateApplication(cl, className);
        // 將 ContextImpl 型別的 context 物件賦給 ContextWrapper::mBase 物件,而大多數時候,實際幹活的
        // 也正是這個物件
        app.attach(context);
        return app;
    }
複製程式碼

Service 建立原始碼

-> ActivityThread::handleCreateService()

    private void handleCreateService(CreateServiceData data) {
        LoadedApk packageInfo = getPackageInfoNoCheck(
                data.info.applicationInfo, data.compatInfo);
        Service service = null;
        try {
            // 建立 Service 物件
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            service = packageInfo.getAppFactory().instantiateService(cl, data.info.name, data.intent);
        } catch (Exception e) {
        }

        try {
            // 建立一個適用於 Application 的 ContextImpl 物件
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            // 將 service 賦給 ContextImpl::mOuterContext 物件,可以看出 mOuterContext 代表的是實際元件物件,
            // 比如具體的 Application、Activity、Service 物件,此處為 Service 物件
            context.setOuterContext(service);
            // 檢查建立 Application 物件,只會建立一次
            Application app = packageInfo.makeApplication(false, mInstrumentation);
            // 將 ContextImpl 型別的 context 物件賦給 ContextWrapper::mBase 物件,而大多數時候,實際幹活的
            // 也正是這個物件
            service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService());
            // 呼叫 onCreate()
            service.onCreate();
        } catch (Exception e) {
        }
    }
複製程式碼

以上程式碼已經過筆者精簡,僅保留了與本文主題相關部分。由以上原始碼可知,ContextWrapper::mBase 物件的具體型別為 ContextImpl,實際幹活的也正是它。至於 Context 怎麼理解,讀者可以理解為類似於環境變數或者機器貓的百變口袋,可以用來獲取各種系統資源。Android 中大多數的記憶體洩露也正是 Context 洩露,比如 View 和 Drawable 物件會保持對其源 Activity 的引用,因此保持 View 或 Drawable 物件,就可能導致 Activity 洩露。

記憶體洩露案例

本來想找個大廠 App 測試下有沒有記憶體洩露,因為筆者以前曾經整合過他們的 SDk,那個時候是存在記憶體洩露的。剛才試了一下,額,退出後連程式都沒有了?...不禁想起筆者讀大學時,那個時候 Android 應用上線,有的連混淆都不做,強如 QQ 都被改的面目全非,各種殺馬特皮膚。筆者也反編譯修改過 Miui 的設定中心,剔除了自啟管理和病毒掃描功能。而今天,各種混淆加密加固,再也無法胡作非為了?。而記憶體洩露這種低階問題,大廠的 App 自然也很少再出現了。

扯遠了,沒辦法,手寫一個記憶體洩露吧:

    @Override
    protected void onStart()
    {
        super.onStart();

        Single.fromCallable(new Callable<SectionResp>()
        {
            @Override
            public SectionResp call() throws Exception
            {
                Thread.sleep(TimeUnit.SECONDS.toMillis(20));
                return new SectionResp();
            }
        }).subscribeOn(Schedulers.newThread()).subscribe(new Consumer<SectionResp>()
        {
            @Override
            public void accept(SectionResp sectionResp) throws Exception
            {
                Logger.debug("RxThread", "thread stopped.");
            }
        });
    }
複製程式碼

上述程式碼,使用 RxJava 模擬弱網請求響應過慢的場景,不是很嚴重的記憶體洩露,因為我們一般都會設定請求超時時間,屆時執行緒自會停止,洩露也隨之消失。不過這並不重要,因為無論什麼形式的洩露,定位方式是相同的。

首先,退出應用,然後 dumpsys meminfo,快速檢視是否存在 Activity 洩露:

D:\Android\projects\wanandroid_java>adb shell dumpsys meminfo -s com.yuloran.wanandroid_java
Applications Memory Usage (in Kilobytes):
Uptime: 578084002 Realtime: 1406250267

** MEMINFO in pid 30243 [com.yuloran.wanandroid_java] **

 App Summary
                       Pss(KB)
                        ------
           Java Heap:    10692
         Native Heap:    27740
                Code:    31264
               Stack:      120
            Graphics:     5816
       Private Other:     8792
              System:    12896

               TOTAL:    97320       TOTAL SWAP PSS:       28

 Objects
               Views:      227         ViewRootImpl:        1
         AppContexts:        3           Activities:        1
              Assets:       18        AssetManagers:        3
       Local Binders:       26        Proxy Binders:       27
       Parcel memory:       10         Parcel count:       42
    Death Recipients:        3      OpenSSL Sockets:        0
            WebViews:        0
複製程式碼

顯然,存在 Activity 洩露,因為 Activity 內通常都會持有其它引用,進而導致大量資源無法及時釋放。

接下來,需要分析具體的記憶體洩露原因,有兩種方法:Android Studio Profiler 和 MAT。

使用 Android Studio Profiler 分析

Android  記憶體洩露詳解

顯然,onStart() 方法中的匿名內部類 new Callable(){} 導致了 MainActivity 洩露。

使用 MAT 分析

說實話,Android Studio 的效能調優工具發展到現在,從最初的 DeviceMonitor 到現在的 Profiler,確實是越來越好用了,想想也就是幾年的時間。在以前自帶工具不好用的時候,都是先把記憶體 dump 為 xxx.hprof,再轉為 MAT 可讀格式,使用 MAT 進行分析。

下載與安裝

下載地址

Android  記憶體洩露詳解

下載 MAT 獨立版,無需安裝,解壓即用。

匯出為 *.hprof

將應用 Java 堆記憶體匯出為 *.hprof(heap profile):

Android  記憶體洩露詳解

也可以使用 adb shell am dumpheap 命令來匯出:

adb shell am dumpheap com.yuloran.wanandroid_java /data/local/tmp/temp.hprof
adb pull /data/local/tmp/temp.hprof temp.hprof
複製程式碼

格式轉換

使用 hprof-conv.exe 將上面匯出的檔案轉為 MAT 可讀的檔案,字尾名一樣:

hprof-conv.exe .\memory-20181231T225508.hprof .\memory-20181231T225508_mat.hprof
複製程式碼

hprof-conv.exe 位於 platform-tools 目錄下:

Android  記憶體洩露詳解

為方便使用,筆者將 platform-tools 新增到了系統環境變數中,所以在上面的 Windows Bat 命令中,可以直接使用此程式。

MAT 分析

開啟上面轉換後的檔案 memory-20181231T225508_mat.hprof:

Android  記憶體洩露詳解

點選直方圖:

Android  記憶體洩露詳解

輸入 .*Activity.* 進行過濾:

Android  記憶體洩露詳解

右擊選擇 with outgoing references:

Android  記憶體洩露詳解

右擊選擇“排除所有虛/弱/軟引用”:

Android  記憶體洩露詳解

檢視 MainActivity 洩露原因:

Android  記憶體洩露詳解

顯然,onStart() 方法中的匿名內部類 new Callable(){} 導致了 MainActivity 洩露。

結語

在公開平臺寫作與個人私下使用是兩種完全不同的感受,需要更為嚴謹的態度以及多方論證。所以筆者的文章大多來自官方文件或原始碼分析,然而畢竟水平有限,如有錯誤,還望不吝賜教?。

Google Android Developers 官網有關記憶體調優的文章:

相關文章