Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)

Shawn_Dut發表於2017-04-10

  上篇部落格我們寫到了 Java/Android 記憶體的分配以及相關 GC 的詳細分析,這篇部落格我們會繼續分析 Android 中記憶體洩漏的檢測以及相關案例,和 Android 的記憶體優化相關內容。
  上篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
  中篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
  下篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)
  轉載請註明出處:blog.csdn.net/self_study/…
  對技術感興趣的同鞋加群544645972一起交流。

Android 記憶體洩漏檢測

  通過上篇部落格我們瞭解了 Android JVM/ART 記憶體的相關知識和洩漏的原因,再來歸類一下記憶體洩漏的源頭,這裡我們簡單將其歸為一下三類:

  • 自身編碼引起
  • 由專案開發人員自身的編碼造成;
  • 第三方程式碼引起
  • 這裡的第三方程式碼包含兩類,第三方非開源的 SDK 和開源的第三方框架;
  • 系統原因
  • 由 Android 系統自身造成的洩漏,如像 WebView、InputMethodManager 等引起的問題,還有某些第三方 ROM 存在的問題。

Android 記憶體洩漏的定位,檢測與修復

  記憶體洩漏不像閃退的 BUG,排查起來相對要困難一些,比較極端的情況是當你的應用 OOM 才發現存在記憶體洩漏問題,到了這種情況才去排查處理問題的話,對使用者的影響就太大了,為此我們應該在編碼階段儘早地發現問題,而不是拖到上線之後去影響使用者體驗,下面總結一下常用記憶體洩漏的定位和檢測工具:

Lint

  Lint 是 Android studio 自帶的靜態程式碼分析工具,使用起來也很方便,選中需要掃描的 module,然後點選頂部選單欄 Analyze -> Inspect Code ,選擇需要掃描的地方即可:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述
      
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

最後在 Performance 裡面有一項是 Handler reference leaks,裡面列出來了可能由於內部 Handler 物件持有外部 Activity 引用導致記憶體洩漏的地方,這些地方都可以根據實際的使用場景去排查一下,因為畢竟不是每個內部 Handler 物件都會導致記憶體洩漏。Lint 還可以自定義掃描規則,使用姿勢很多很強大,感興趣的可以去了解一下,除了 Lint 之外,還有像 FindBugs、Checkstyle 等靜態程式碼分析工具也是很不錯的。

StrictMode

  StrictMode 是 Android 系統提供的 API,在開發環境下引入可以更早的暴露發現問題給開發者,於開發階段解決它,StrictMode 最常被使用來檢測在主執行緒中進行讀寫磁碟或者網路操作等耗時任務,把這些耗時任務放置於主執行緒會造成主執行緒阻塞卡頓甚至可能出現 ANR ,官方例子:

 public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }複製程式碼

把上面這段程式碼放在早期初始化的 Application、Activity 或者其他應用元件的 onCreate 函式裡面來啟用 StrictMode 功能,一般 StrictMode 只是在測試環境下啟用,到了線上環境就不要開啟這個功能。啟用 StrictMode 之後,在 logcat 過濾日誌的地方加上 StrictMode 的過濾 tag,如果發現一堆紅色告警的 log,說明可能就出現了記憶體洩漏或者其他的相關問題了:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

比如上面這個就是因為呼叫 registerReceiver 之後忘記呼叫 unRegisterReceiver 導致的 activity 洩漏,根據錯誤資訊便可以定位和修復問題。

LeakCanary

   LeakCanary 是一個 Android 記憶體洩漏檢測的神器,正確使用可以大大減少記憶體洩漏和 OOM 問題,地址:

https://github.com/square/leakcanary複製程式碼

整合 LeakCanary 也很簡單,在 build.gradle 檔案中加入:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }複製程式碼

然後在 Application 類中新增下面程式碼:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}複製程式碼

上面兩步做完之後就算是整合了 LeakCanary 了,非常簡單方便,如果程式出現了記憶體洩漏會彈出 notification,點選這個 notification 就會進入到下面這個介面,或者整合 LeakCanary 之後在桌面會有一個 LeakCanary 的圖示,點選進去是所有的記憶體洩漏列表,點選其中一項同樣是進入到下面介面:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

這個介面就會詳細展示引用持有鏈,一目瞭然,對於問題的解決方便了很多,堪稱神器,更多實用姿勢可以看看 LeakCanary FAQ
  還有一點需要提到的是,LeakCanary 在檢測記憶體洩漏的時候會阻塞主介面,這是一點體驗有點不爽的地方,但是這時候阻塞肯定是必要的,因為此時必須要掛起執行緒來獲取當前堆的狀態。然後也並不是每個 LeakCanary 提示的地方都有記憶體洩漏,這時候可能需要藉助 MAT 等工具去具體分析。不過 LeakCanary 有一點非常好的地方是因為 Android 系統也會有一些記憶體洩漏,而 LeakCanary 對此則提供了一個 AndroidExcludedRefs 類來幫助我們排除這些問題。

Android Memory Monitor

  Memory Monitor 是 Android Studio 自帶的一個監控記憶體使用狀態的工具,入口如下所示:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

在 Android Monitor 點開之後 logcat 的右側就是 Monitor 工具,其中可以檢測記憶體、CPU、網路等內容,我們這裡只用到了 Memory Monitor 功能,點選紅色箭頭所指的區域,就會 dump 此時此刻的 Memory 資訊,並且生成一個 .hprof 檔案,dump 完成之後會自動開啟這個檔案的顯示介面,如果沒有開啟,可以通過點選最左側的 Capture 介面或者 Tool Window 裡面的 Capture 進入 dump 的 .hprof 檔案列表:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

  接著我們來分析一下這個生成的 .hprof 檔案所展示的資訊:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

首先左上角的下拉框,可以選擇 App Heap、Image Heap 和 Zygote Heap,對應的就是上篇部落格講到的 Allocation Space,Image Space 和 Zygote Space,我們這裡選擇 Allocation Space,然後第二個選擇 PackageTreeView 這一項,展開之後就能看見一個樹形結構了,然後繼續展開我們應用包名的對應物件,就可以很清晰的看到有多少個 Activity 物件了,上面那兩欄展示的資訊按照從左到右的順序,定義如下所示:

Column Description
Class Name 佔有這塊記憶體的類名
Total Count 未被處理的數量
Heap Count 在上面選擇的指定 heap 中的數量
Sizeof 這個物件的大小,如果在變化中,就顯示 0
Shallow Size 在當前這個 heap 中的所有該物件的總數
Retained Size 這個類的所有物件佔有的總記憶體大小
Instance 這個類的指定物件
Reference Tree 指向這個選中物件的引用,還有指向這個引用的引用
Depth 從 GC Root 到該物件的引用鏈路的最短步數
Shallow Size 這個引用的大小
Dominating Size 這個引用佔有的記憶體大小

然後可以點選展開右側的 Analyzer Tasks 項,勾選上需要檢測的任務,然後系統就會給你分析出結果:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

從分析的結果可以看到洩漏的 Activity 有兩個,非常直觀,然後點開其中一個,觀察下面的 ReferenceTree 選項:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

可以看到 Thread 物件持有了 SecondActivity 物件的引用,也就是 GC Root 持有了該 Activity 的引用,導致這個 Activity 無法回收,問題的根源我們就發現了,接下來去處理它就好了。
  關於更多 Android Memory Monitor 的使用可以去看看這個官方文件:HPROF Viewer and Analyzer

MAT

  MAT(Memory Analyzer Tools)是一個 Eclipse 外掛,它是一個快速、功能豐富的 JAVA heap 分析工具,它可以幫助我們查詢記憶體洩漏和減少記憶體消耗,MAT 外掛的下載地址:Eclipse Memory Analyzer Open Source Project,上面通過 Android studio 生成的 .hprof 檔案因為格式稍有不同,所以需要經過一個簡單的轉換,然後就可以通過 MAT 去開啟了:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

通過 MAT 去開啟轉換之後的這個檔案:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

用的最多的就是 Histogram 功能,點選 Actions 下的 Histogram 項就可以得到 Histogram 結果:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

我們可以在左上角寫入一個正規表示式,然後就可以對所有的 Class Name 進行篩選了,很方便,頂欄展示的資訊 "Objects" 代表該類名物件的數量,剩下的 "Shallow Heap" 和 "Retained Heap" 則和 Android Memory Monitor 類似。我們們接著點選 SecondActivity,然後右鍵:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

在彈出來的選單中選擇 List objects->with incoming references 將該類的例項全部列出來:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

通過這個列表我們可以看到 SecondActivity@0x12faa900 這個物件被一個 this$00x12c65140 的匿名內部類物件持有,然後展開這一項,發現這個物件是一個 handler 物件:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

快速定位找到這個物件沒有被釋放的原因,可以右鍵 Path to GC Roots->exclude all phantom/weak/soft etc. references 來顯示出這個物件到 GC Root 的引用鏈,因為強引用才會導致物件無法釋放,所以這裡我們要排除其他三種引用:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

這麼處理之後的結果就很明顯了:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

一個非常明顯的強引用持有鏈,GC Root 我們前面的部落格中說到包含了執行緒,所以這裡的 Thread 物件 GC Root 持有了 SecondActivity 的引用,導致該 Activity 無法被釋放。
  MAT 還有一個功能就是能夠對比兩個 .hprof 檔案,將兩個檔案都新增到 Compare Basket 裡面:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

新增進去之後點選右上角的 ! 按鈕,然後就會生成兩個檔案的對比:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

同樣適用正規表示式將需要的類篩選出來:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

結果也很明顯,退出 Activity 之後該 Activity 物件未被回收,仍然在記憶體中,或者可以調整對比選項讓對比結果更加明顯:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

也可以對比兩個物件集合,方法與此類似,都是將兩個 Dump 結果中的物件集合新增到 Compare Basket 中去對比,找出差異後用 Histogram 查詢的方法找出 GC Root,定位到具體的某個物件上。

adb shell && Memory Usage

  可以通過命令 adb shell dumpsys meminfo [package name] 來將指定 package name 的記憶體資訊列印出來,這種模式可以非常直觀地看到 Activity 未釋放導致的記憶體洩漏:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

或者也可以通過 Android studio 的 Memory Usage 功能進行檢視,最後的結果是一樣的:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

Allocation Tracker

  Android studio 還自帶一個 Allocation Tracker 工具,功能和 DDMS 中的基本差不多,這個工具可以監控一段時間之內的記憶體分配:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

在記憶體圖中點選途中標紅的部分,啟動追蹤,再次點選就是停止追蹤,隨後自動生成一個 .alloc 檔案,這個檔案就記錄了這次追蹤到的所有資料,然後會在右上角開啟一個資料皮膚:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

這個工具詳細的介紹可以看看這個部落格:Android效能專項測試之Allocation Tracker(Android Studio)

常見的記憶體洩漏案例

  我們來看看常見的導致記憶體洩漏的案例:

靜態變數造成的記憶體洩漏

  由於靜態變數的生命週期和應用一樣長,所以如果靜態變數持有 Activity 或者 Activity 中 View 物件的應用,就會導致該靜態變數一直直接或者間接持有 Activity 的引用,導致該 Activity 無法釋放,從而引發記憶體洩漏,不過需要注意的是在大多數這種情況下由於靜態變數只是持有了一個 Activity 的引用,所以導致的結果只是一個 Activity 物件未能在退出之後釋放,這種問題一般不會導致 OOM 問題,只能通過上面介紹過的幾種工具在開發中去觀察發現。
  這種問題的解決思路很簡單,就是不讓靜態變數直接或者間接持有 Activity 的強引用,可以將其修改為 soft reference 或者 weak reference 等等之類的,或者如果可以的話將 Activity Context 更換為 Application Context,這樣就能保證生命週期一致不會導致記憶體洩漏的問題了。

內部類持有外部類引用

  我們上面的 demo 中模擬的就是內部類物件持有外部類物件的引用導致外部類物件無法釋放的問題,在 Java 中非靜態內部類和匿名內部類會持有他們所屬外部類物件的引用,如果這個非靜態內部類物件或者匿名內部類物件被一個耗時的執行緒(或者其他 GC Root)直接或者間接的引用,甚至這些內部類物件本身就在做一些耗時操作,這樣就會導致這個內部類物件直接或者間接無法釋放,內部類物件無法釋放,外部類的物件也就無法釋放造成記憶體洩漏,而且如果無法釋放的物件積累起來就會造成 OOM,示例程式碼如下所示:

public class SecondActivity extends AppCompatActivity{
    private Handler handler;
    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);//decode 一個大圖來模擬記憶體無法釋放導致的崩潰
        findViewById(R.id.btn_second).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });

        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);

            }
        };
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                handler.sendEmptyMessage(0);
            }
        }).start();
    }
}複製程式碼

  這個問題的解決方法可以根據實際情況進行選擇:

  • 將非靜態內部類或者匿名內部類修改為靜態內部類,比如 Handler 修改為靜態內部類,然後讓 Handler 持有外部 Activity 的一個 Weak Reference 或者 Soft Reference;
  • 在 Activity 頁面銷燬的時候將耗時任務停止,這樣就能保證 GC Root 不會間接持有 Activity 的引用,也就不會導致記憶體洩漏;

錯誤使用 Activity Context

  這個很好理解,在一個錯誤的地方使用 Activity Context,造成 Activity Context 被靜態變數長時間引用導致無法釋放而引發的記憶體洩漏,這個問題的處理方式也很簡單,如果可以的話修改為 Application Context 或者將強引用變成其他引用。

資源物件沒關閉造成的記憶體洩漏

  資源性物件比如(Cursor,File 檔案等)往往都用了一些緩衝,我們在不使用的時候應該及時關閉它們,以便它們的緩衝物件被及時回收,這些緩衝不僅存在於 java 虛擬機器內,還存在於 java 虛擬機器外,如果我們僅僅是把它的引用設定為 null 而不關閉它們,往往會造成記憶體洩漏。但是有些資源性物件,比如 SQLiteCursor(在解構函式 finalize(),如果我們沒有關閉它,它自己會調 close() 關閉),如果我們沒有關閉它系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性物件在不使用的時候,應該呼叫它的 close() 函式,將其關閉掉,然後再置為 null,在我們的程式退出時一定要確保我們的資源性物件已經關閉。
  程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢 Cursor 後沒有關閉的情況,如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在常時間大量操作的情況下才會出現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險,示例程式碼:

Cursor cursor = getContentResolver().query(uri...); 
if (cursor.moveToNext()) { 
... ... 
}複製程式碼

更正程式碼:

Cursor cursor = null;
try {
    cursor = getContentResolver().query(uri...);
    if (cursor != null && cursor.moveToNext()) {
        ... ...
    }
} finally {
    if (cursor != null) {
        try {
            cursor.close();
        } catch (Exception e) {
            //ignore this
        }
    }
}複製程式碼

集合中物件沒清理造成的記憶體洩漏

  在實際開發過程中難免會有把物件新增到集合容器(比如 ArrayList)中的需求,如果在一個物件使用結束之後未將該物件從該容器中移除掉,就會造成該物件不能被正確回收,從而造成記憶體洩漏,解決辦法當然就是在使用完之後將該物件從容器中移除。

WebView造成的記憶體洩露

  具體的可以看看我的這篇部落格:android WebView詳解,常見漏洞詳解和安全原始碼(下)

未取消註冊導致的記憶體洩漏

  一些 Android 程式可能引用我們的 Android 程式的物件(比如序號產生器制),即使我們的 Android 程式已經結束了,但是別的應用程式仍然還持有對我們 Android 程式某個物件的引用,這樣也會造成記憶體不能被回收,比如呼叫 registerReceiver 後未呼叫unregisterReceiver。假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊,則可以在 LockScreen 中定義一個 PhoneStateListener 的物件,同時將它註冊到 TelephonyManager 服務中,對於 LockScreen 物件,當需要顯示鎖屏介面的時候就會建立一個 LockScreen 物件,而當鎖屏介面消失的時候 LockScreen 物件就會被釋放掉,但是如果在釋放 LockScreen 物件的時候忘記取消我們之前註冊的 PhoneStateListener 物件,則會間接導致 LockScreen 無法被回收,如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的 LockScreen 物件沒有辦法被回收而引起 OOM,雖然有些系統程式本身好像是可以自動取消註冊的(當然不及時),但是我們還是應該在程式結束時明確的取消註冊。

因為記憶體碎片導致分配記憶體不足

  還有一種情況是因為頻繁的記憶體分配和釋放,導致記憶體區域裡面存在很多碎片,當這些碎片足夠多,new 一個大物件的時候,所有的碎片中沒有一個碎片足夠大以分配給這個物件,但是所有的碎片空間加起來又是足夠的時候,就會出現 OOM,而且這種 OOM 從某種意義上講,是完全能夠避免的。
  由於產生記憶體碎片的場景很多,從 Memory Monitor 來看,下面場景的記憶體抖動是很容易產生記憶體碎片的:

Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

最常見產生記憶體抖動的例子就是在 ListView 的 getView 方法中未複用 convertView 導致 View 的頻繁建立和釋放,針對這個問題的處理方式那當然就是複用 convertView;或者是 String 拼接建立大量小的物件(比如在一些頻繁呼叫的地方打字串拼接的 log 的時候);如果是其他的問題,就需要通過 Memory Monitor 去觀察記憶體的實時分配釋放情況,找到記憶體抖動的地方修復它,或者如果當出現下面這種情況下的 OOM 時,也是由於記憶體碎片導致無法分配記憶體:
Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
這裡寫圖片描述

出現上面這種型別的 Crash 時就要去分析應用裡面是不是存在大量分配釋放物件的地方了。

Android 記憶體優化

  記憶體優化請看下篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)

引用

blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
mp.weixin.qq.com/s?__biz=MzA…
geek.csdn.net/news/detail…
www.jianshu.com/p/216b03c22…
zhuanlan.zhihu.com/p/25213586
joyrun.github.io/2016/08/08/…
www.cnblogs.com/larack/p/60…
source.android.com/devices/tec…
blog.csdn.net/high2011/ar…
gityuan.com/2015/10/03/…
www.ayqy.net/blog/androi…
developer.android.com/studio/prof…
zhuanlan.zhihu.com/p/26043999

相關文章