1. 程式人生 > >Andorid效能優化(三) 之 如何定位記憶體洩漏

Andorid效能優化(三) 之 如何定位記憶體洩漏

1 定位記憶體洩漏工具

正所謂工欲善其事,必先利其器。定位記憶體洩漏,可以藉助目前比較流行的一些工具來幫助發現和定位問題,下面我們就來看看這些工具。

1.1 Memory Profiler

Android Studio 3.0 採用全新的Android Profiler視窗取代 Android Monitor 工具。 這些全新的分析工具能夠提供關於應用 CPU、記憶體和網路 Activity 的實時資料。你可以執行基於樣本的函式跟蹤來記錄程式碼執行時間、採集堆轉儲資料、檢視記憶體分配,以及檢視網路傳輸檔案的詳情。Memory Profiler 是 Android Profiler 中的一個元件,可幫助你識別導致應用卡頓、凍結甚至崩潰的記憶體洩漏和流失。 它顯示一個應用記憶體使用量的實時圖表,讓你可以捕獲堆轉儲、強制執行垃圾回收以及跟蹤記憶體分配。要開啟 Memory Profiler,可以點選 View > Tool Windows > Android Profiler

(也可以點選工具欄中的 Android Profiler https://img-blog.csdnimg.cn/2018122518253191)。開啟Android Profiler,如下圖。再從工具欄中選擇你想要分析的設定和應用程程序,然後點選MEMORY時間線中的任意位置即可開啟Memory Profiler。

  

1.1.1 Memory Profiler 概覽

當你首次開啟 Memory Profiler 時,你將看到一條表示應用記憶體使用量的詳細時間線,並可訪問用於強制執行垃圾回收、捕捉堆轉儲和記錄記憶體分配的各種工具。

 如圖所示,Memory Profiler 的預設檢視包括以下各項:

1.用於強制執行垃圾回收 Event 的按鈕。

2.用於捕獲堆轉儲的按鈕。

3.用於記錄記憶體分配情況的按鈕(此按鈕僅在連線至執行 Android 7.1 或更低版本的裝置時才會顯示)。

4.用於放大/縮小時間線的按鈕。

5.用於跳轉至實時記憶體資料的按鈕。

6.Event 時間線,其顯示 Activity 狀態、使用者輸入 Event 和螢幕旋轉 Event。

7.記憶體使用量時間線,其包含以下內容:

        a.一個顯示每個記憶體類別使用多少記憶體的堆疊圖表,如左側的 y 軸以及頂部的彩色鍵所示。

        b.虛線表示分配的物件數,如右側的 y 軸所示。

        c.用於表示每個垃圾回收 Event 的圖示。

8.展示應用根據Android系統機制所提交的所有私有記憶體頁面數。 此計數不包含與系統或其他應用共享的頁面。

        Java:從 Java 或 Kotlin 程式碼分配的物件記憶體。

        Native:從 C 或 C++ 程式碼分配的物件記憶體。

        Graphics:圖形緩衝區佇列向螢幕顯示畫素(包括 GL 表面、GL 紋理等等)所使用的記憶體。 (請注意,這是與 CPU 共享的記憶體,不是 GPU 專用記憶體。)

        Stack: 應用中的原生堆疊和 Java 堆疊使用的記憶體。 這通常與應用執行多少執行緒有關。

        Code:應用用於處理程式碼和資源(如 dex 位元組碼、已優化或已編譯的 dex 碼、.so 庫和字型)的記憶體。

        Other:應用使用的系統不確定如何分類的記憶體。

        Allocated:應用分配的 Java/Kotlin 物件數。 它沒有計入 C 或 C++ 中分配的物件。當連線至執行 Android 7.1 及更低版本的裝置時,此分配僅在 Memory Profiler 連線至你執行的應用時才開始計數。 因此,你開始分析之前分配的任何物件都不會被計入。 不過,Android 8.0 附帶一個裝置內建分析工具,該工具可記錄所有分配,因此,在 Android 8.0 及更高版本上,此數字始終表示你的應用中待處理的 Java 物件總數。

1.1.2 檢視記憶體分配

記憶體分配顯示記憶體中每個物件是如何分配的。 具體而言,Memory Profiler 可為你顯示有關物件分配的以下資訊:

  1. 分配哪些型別的物件以及它們使用多少空間。
  2. 每個分配的堆疊追蹤,包括在哪個執行緒中。
  3. 物件在何時被取消分配(僅當使用執行 Android 8.0 或更高版本的裝置時)。

如果你的裝置執行 Android 8.0 或更高版本,你只需點選並按住時間線,並拖動選擇你想要檢視分配的區域,操作後如下圖。 不需要開始記錄會話,因為 Android 8.0 及更高版本附帶裝置內建分析工具,可持續跟蹤你的應用分配。

如果你的裝置執行 Android 7.1 或更低版本,則在 Memory Profiler 工具欄中點選 Record memory allocations 。 記錄時,Android Monitor 將跟蹤你的應用中進行的所有分配。 操作完成後,點選 Stop recording ,以檢視分配。操作如圖中紅圈位置按鈕。

完成上面的區域跟蹤操作後,便可以繼續來檢查記憶體分配情況了。請看下方出現分配物件的列表,列表中可以按下每個列的表頭進行排序,根據你的實際查詢需求排序即可。然後點選其中一項,此時在右側將出現 Instance View 窗格,顯示該類的每個例項。在Instance View 窗格中,點選一個例項。 此時下方將出現 Call Stack 標籤,顯示該例項被分配到何處以及哪個執行緒中。在 Call Stack 標籤中,點選任意行以在編輯器中跳轉到該程式碼。如下圖。

1.1.3 捕獲堆轉儲

堆轉儲顯示在你捕獲堆轉儲時你的應用中哪些物件正在使用記憶體。 特別是在長時間的使用者會話後,堆轉儲會顯示你認為不應再位於記憶體中卻仍在記憶體中的物件,從而幫助識別記憶體洩漏。在捕獲堆轉儲後,你可以檢視以下資訊:

  1. 你的應用已分配哪些型別的物件,以及每個型別分配多少。
  2. 每個物件正在使用多少記憶體。
  3. 在程式碼中的何處仍在引用每個物件。
  4. 物件所分配到的呼叫堆疊。 (目前,如果你在記錄分配時捕獲堆轉儲,則只有在 Android 7.1 及更低版本中,堆轉儲才能使用呼叫堆疊。)

要捕獲堆轉儲,在 Memory Profiler 工具欄中點選 Dump Java heap https://img-blog.csdnimg.cn/20181225182531111 ,轉儲後如下圖。

在圖中分配物件的列表中,我們來關注一下兩個列Shallow Size和Retained Size的代表意思。

Shallow Size表示物件在沒有引用其他物件的情況下本身佔用的記憶體大小。

Retained Size是指物件自己本身的Shallow heap的大小+物件直所引用到的物件的大小。

1.1.4 將堆轉儲另存為hprof檔案

在捕獲堆轉儲後,僅當分析器執行時才能在 Memory Profiler 中檢視資料。 當你退出分析會話時,你將丟失堆轉儲。 因此,如果你要儲存堆轉儲以供日後檢視,可通過右擊左邊SESSIONS中想要匯出的Dump,然後選擇Export,如下圖。

要使用其他hporf分析器,你需要hporf檔案從 Android 格式轉換為 Java SE HPROF 格式。 你可以使用 android_sdk/platform-tools/ 目錄中提供的 hprof-conv 工具執行此操作。 執行包括以下兩個引數的 hprof-conv命令:原始 HPROF 檔案和轉換後 HPROF 檔案的寫入位置。 例如:

hprof-conv  xx.hprof  xx_new.hprof

關於Memory Profile的詳細介紹過參考官方文件:

https://developer.android.google.cn/studio/profile/memory-profiler

1.2 Memory Analyzer Tool

Memory Analyzer Tool簡稱MAT,它是一款非常強大的記憶體洩漏分析工具,其最大的作用就是通過程式中捕捉生成的hprof檔案進行分析或比較能夠定位大記憶體物件和記憶體洩漏源頭。在Android Studio 3.0之前,要做記憶體分析幾乎都是使用MAT。下載地址:http://www.eclipse.org/mat/downloads.php

1.2.1 生成hprof檔案

方法1

如果你使用的Android Studio 是3.0或更新的版本,就像前面介紹Memory Profiler中,“將堆轉儲另存為hprof檔案”即可。

方法2

如果你使用的Android Studio是3.0之前的版本,從Tools -> Android -> Android Device Monitor中開啟DDMS(Dalvik Debug Monitor Service)介面,如下圖,選擇要進行分析的app,然後點選“Dump HPROF file”按鈕,等待一小段時間即可匯出一個hprof檔案。

同樣,因為匯出的hprof檔案不能被MAT直接識別,你需要hporf檔案從 Android 格式轉換為 Java SE HPROF 格式。 你可以使用 android_sdk/platform-tools/ 目錄中提供的 hprof-conv 工具執行此操作。 執行包括以下兩個引數的 hprof-conv命令:原始 HPROF 檔案和轉換後 HPROF 檔案的寫入位置。 例如:

hprof-conv  xx.hprof  xx_new.hprof

方法3

如果你使用的Android Studio是3.0之前的版本,在Android Studio視窗中的Android Monitor中選中要分析的app,然後點選“Initiate GC”,然後再點選“Dump Java Heap”按鈕,如下圖1,也是等待一小段時間後,即可生成一個hprof檔案。如下圖2,點選剛生成的hprof檔案,然後選擇“Exprot to standard.hprof”就可直接匯出一個不需要轉換的hprof檔案了。

1.2.2 開始分析hprof檔案

開啟MAT,通過選單開啟剛才儲存的hprof檔案,如圖。MAT最常用功能的有Histogram和Dominator Tree,通過Histogram可直觀看出記憶體中不同型別的buffer的數量和佔用記憶體大小,而Dominator Tree則把記憶體中的物件按照從大到小順序進行排序,並可分析物件之間的引用關係,記憶體洩露分析就是通過Dominator Tree來完成。

1.2.3 Dominator Tree使用說明

在Dominator Tree中記憶體洩露原因一般不會直接顯示出來,這時需要從大到小去排查一遍。列表中出現的就是所在類的例項資訊,可以選定其中一你認為可能存在問題的項,然後右擊滑鼠->Path To GC Roots -> exclude wakd/soft references,如圖操作。Path To GC Roots過程中之所以選擇排除弱引用和軟引用,是因為二者都是較大機率被gc回收掉,它們並不能造成記憶體洩漏,排除後那麼剩下的就是強引用了。

圖中能看到列表中有四個列,我們來看看Shallow Heap和Retained Heap兩列的代表意思。

Shallow Heap表示物件在沒有引用其他物件的情況下本身佔用的記憶體(包含的方法、元素)大小。

Retained heap是指物件自己本身的Shallow heap的大小+物件直所引用到的物件的大小。

Dominator Tree還可以使用搜尋功能,比如我們知道MainActivity存在記憶體洩露,那麼我們就可以直接搜尋MainActivity,如圖。

1.2.4 hprof檔案對比

比如現在分析ActivityA的記憶體洩露問題,可以參考如下步驟:

1、進入ActivityA之前,先dump一個hprof檔案HprofA;

2、進入ActivityA操作一會,再退出ActivityA後dump個hprof檔案HprofB;

3、採用Histogram對比分析這兩個Hprof檔案,即可得出ActivityA是否洩露

如圖。操作步驟:

步驟一、先開啟HprofB檔案,接著再開啟Histogram,然後點選“Compare to another Heap Dump”按鈕

步驟二、在彈出的對話方塊中,選擇要進行對比的HprofA,然後便可以看到兩個hporf檔案在記憶體方面的對比,如圖(這時使用自己的拷貝版來對比,所以都是0)。這裡也可以使用搜索功能,一旦發現存在大於0,則可以斷定存在洩露情況,找出洩露的地方,就可以使用Dominator TreePath To GC Roots -> exclude wakd/soft references再進行分析。

1.3 Strict Mode

Strict Mode叫作嚴格模式,是用於檢測開發過程中程式碼違規的工具,一般情況下用於Debug模式下的開發。它的作用主要是檢測兩大問題,一是ThreadPolicy,另一個是VmPolicy。

ThreadPolicy用於監控啟用嚴格模式的執行緒是否進行磁碟讀寫、網格訪問等操作,一般用於主執行緒的監控。

VmPolicy可以發現某此型別的記憶體洩漏。例如IO類操作是否沒有及時呼叫close方法、Activity是否無法回收、BroadcastReceiver是否沒有進行反註冊、類的例項個數是否上限是否超出上限,等。

Strict Mode的使用一般會在Application中進行啟用,如程式碼:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        initStricMode();
    }

    private void initStricMode() {
        if (!BuildConfig.DEBUG) {
            return;
        }

        // ThreadPolicy
        StrictMode.ThreadPolicy.Builder threadPolicyBuilder = new StrictMode.ThreadPolicy.Builder();
        threadPolicyBuilder
                .detectAll()            // 檢測所有項
//                .permitAll()            // 允許所有項

//                .penaltyDialog()        // 處罰通知:彈出違規對話方塊
//                .penaltyDropBox()       // 處罰通知:將違規資訊記錄到 dropbox 系統日誌目錄中(/data/system/dropbox)
//                .penaltyDeath()         // 處罰通知:直接 Crash 掉當前應用程式
//                .penaltyFlashScreen()   // 處罰通知:螢幕閃爍,視裝置是否可行
                .penaltyLog();          // 處罰通知:列印違規資訊日誌
        StrictMode.setThreadPolicy(threadPolicyBuilder.build());

        // VmPolicy
        StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder();
        vmPolicyBuilder
                .detectAll()
                .penaltyLog();
        StrictMode.setVmPolicy(vmPolicyBuilder.build());
    }
}

ThreadPolicy和VmPolicy裡頭都有一系列的detectXX方法、permitXX方法 和 penaltyXX方法。上述程式碼中拿ThreadPolicy來看,使用了detectAll()方法,表示為要檢測所有的專案,我們可以看下它的原始碼:

public Builder detectAll() {
    detectDiskReads();
    detectDiskWrites();
    detectNetwork();

    final int targetSdk = VMRuntime.getRuntime().getTargetSdkVersion();
    if (targetSdk >= Build.VERSION_CODES.HONEYCOMB) {
        detectCustomSlowCalls();
    }
    if (targetSdk >= Build.VERSION_CODES.M) {
        detectResourceMismatches();
    }
    if (targetSdk >= Build.VERSION_CODES.O) {
        detectUnbufferedIo();
    }
    return this;
}

原始碼中可以看到detectAll方法中包含了數個detectXX方法,從它們的命名或註釋中便能清楚它們的用途了,這裡不作一一介紹。我們如果要單使用這些方法時一定要注意判斷Android API版本號。

繼續看回示例程式碼ThreadPolicy中有註釋了permitAll()方法,同時它的內部邏輯也是就包含了數個permitXX方法,此類方法表示允許XX項,也就是不檢測XX項,剛好跟detectXX系列方法相反。

penaltyXX系列方法代表處罰通知,一般情交下使用penaltyLog()方法表示使用日誌輸出處罰通知即可。

啟動嚴格模式後,若再次使用上面“內部類導致外部無法被釋放”中未經修改的TestActivity時,就會輸出如下日誌:

1.4 LeakCanary

LeakCanary是比較方便的用於檢測記憶體洩漏的內嵌工具。它的使用也是非常的簡單,只需要兩個步驟。

首先在build.gradle中配置如下程式碼:

dependencies {
    ……
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.6'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.6'
}

其實就是初始化,一般會在Application中進行,如程式碼:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initLeakCanary();
    }

    private void initLeakCanary() {
        if (!BuildConfig.DEBUG) {
            return;
        }
        LeakCanary.install(this);
    }
}

準備工作完成後,我們還是拿上面“內部類導致外部無法被釋放”中未經修改的TestActivity為反面示例,再次執行後,使記憶體洩漏場景生效後,便會在螢幕中間彈出如下的Toast:

隨後,彈出一條通知欄和在桌面中生成一個名字為“Leaks”的快捷方式,通知欄樣式如圖:

點選剛彈出的通知或點選新生成的桌面快捷圖示便打開了一個詳細定位問題頁面,如下圖。

2 定位和分析記憶體洩漏的技巧

我們想要定位記憶體洩漏時,一般都是要對應用程式碼施加壓力並嘗試強制記憶體洩漏。 在應用中引發記憶體洩漏的一種方式是,先讓其執行一段時間,然後再檢查堆。 洩漏在堆中可能逐漸匯聚到分配頂部。 不過,洩漏越小,你越需要執行更長時間的應用才能看到洩漏。你還可以通過以下方式之一觸發記憶體洩漏:

  1. 將裝置從縱向旋轉為橫向,然後在不同的 Activity 狀態下反覆操作多次。 旋轉裝置經常會導致應用洩漏 Activity、Context 或 View 物件,因為系統會重新建立 Activity,而如果你的應用在其他地方保持對這些物件之一的引用,系統將無法對其進行垃圾回收。
  2. 處於不同的 Activity 狀態時,在你的應用與另一個應用之間切換(導航到主螢幕,然後返回到你的應用)。

總之,請記住一句話:定位和分析記憶體洩漏需要耐心和細心!