檢測記憶體是否泄漏非常簡單,只要在任意位置呼叫 Debug.dumpHprofData(file) 即可,通過拿到 hprof 檔案進行分析就可以知道哪里產生了泄漏,但 dump 的程序會 suspend 所有的 java 執行緒,導致用戶界面無回應,所以又不能隨意 dump,為了能找到合理的 dump 時機,leakCanary 就采用預判的方式,在 onDestroy 中先檢測一下當前 Activity 是否存在泄漏的風險,如果有這種情況,就開始 dump,需要注意的是,在 onDestroy 做檢測僅僅只是預判,一種時機,并不能斷定真的發生了泄漏,真正的泄漏需要通過分析 hprof 檔案才能知曉, ?
hprof 是由 JVM TI Agent HPROF 生成的一種二進制檔案,檔案格式可以查看 Binary Dump Format:
一、如何預判記憶體泄漏
- 主動檢測法
- 閾值檢測法
1、主動檢測法
- Activity 的檢測預判
- Service 的檢測預判
- Bitmap 大圖的檢測預判
1、Activity 的檢測預判 LeakCanary 中對 Activity 的預判是在 onDestroy 生命周期中通過弱參考佇列來持有當前 Activity 參考,如果在主動觸發 gc 之后,泄漏物件集合中仍然能找到該參考實體,則說明發生了記憶體泄漏,就開始 dump ?
2、Service 的檢測預判 LeakCanary 對 Service 的記憶體泄漏檢測時機,是 hook 監聽 ActivityThread 的 stopService,然后記錄這個 binder 到弱參考集合中,然后代理 AMS 的 serviceDoneExecuting 方法,通過 binder 在弱參考集合中去移除,移除成功的話,說明發生了記憶體泄漏,就開始 dump ?
3、Bitmap 大圖檢測預判 Bitmap 不像 Activity、Service 這種,能夠通過生命周期主動監測當前是否有記憶體泄漏的可能,他一般是在 Activity、Service 發生泄漏 dump 的時候,順便檢測一下 Bitmap ,在 Koom 中,Bitmap 大圖檢測是分析 hprof 中是否有超過 Bitmap 設定的閾值 size (width * height) ?
2、閾值檢測法
閾值檢測法的代表框架是 Koom,他拋棄了 LeakCanary 的實時檢測性,采用定時輪訓檢測當前記憶體是否在不斷累加,增長達到一定次數(可自己配置)時會進行 dump hprof,這種方式會犧牲一定的時效性,但對于應用到線上的 Koom 的框架,他完全不需要這么高的時效性 ?
二、如何分析記憶體泄漏
分析工具代表:
- MAT
- Android Studio
- HaHa
- Matrix
- LeakCanary 1.x
- shark
- Liko
- Koom
- LeakCanary 2.x
1、MAT
MAT 工具下載可點擊鏈接 ,Android 生成的 dump 需要做一下轉換才能被 MAT 識別,轉換指令:
hprof-conv <hprof 檔案> <新生成的檔案>
eg:
hprof-conv android.hprof mat.hprof
hprof-conv 跟 adb 在同一個檔案夾下,配置了 adb 命令的可以直接用這個命令執行, ?
MAT 查記憶體泄漏會有點費勁,畢竟是個 java 通用工具,并不會指明告訴你是哪個 Activity 發生了泄漏,但可以分析個大概, ?
一般泄漏的都是比較大的實體:

點擊類名進入查看: ?
在這里插入圖片描述
ActivityLeakMaker 占用了近 190944 byte 的記憶體空間,并且參考鏈里面有 Activity 相關的內容,切回代碼來看問題,原來是靜態變數持有了 Activity 實體導致:

2、Android Studio
Android Studio 的 Profiler 工具支持 hprof 的決議,并且很智能的提示當前 leak 了哪些物件,打開方式很簡單,將 hprof 檔案拖拽至 as,然后雙擊 hprof 檔案即可:

我們可以很直觀的看到,當前 LeakedActivity 和 ReportFragment 發生了泄漏, ?
如果我們的需求僅僅只是在開發階段進行記憶體泄漏檢測的話,并且又不想接入 LeakCanary(因為有時候想除錯下自己模塊的代碼,其他模塊經常報記憶體泄漏,凍結當前執行緒,很影響除錯),那么我們可以在應用里面埋個彩蛋,比如單擊 5 次版本號,然后呼叫 Debug.dumpHprofData ,然后將 hprof 檔案匯出到 as 進行分析,這就將原本可能會進行數次 dump 的程序,改成了自己需要去檢測的時候再去 dump, ?
3、HaHa
在 LeakCanary 的第一版的時候,是采用的 Haha 庫來分析泄漏參考鏈,但由于后面新出的 Shark,比 HaHa 快 8 倍,并且記憶體占用還要少 10 倍,但查找泄漏路徑的大致步驟與 Shark 無異,故此文就不分析 HaHa 了, ?
4、Shark
Shark 是 square 團隊開發的一款全新的分析 hprof 檔案的工具,其官方宣布比 Android Studio 用于 memory profiler 的核心庫 perflib 要快 8 倍并且記憶體占用少 10 倍,更加適合手機端的分析工具,其目的就是提供快速決議hprof檔案和分析快照的能力,并找出真正的泄漏物件以及物件到GcRoot 的最短參考路徑鏈,以便幫助開發者更加直觀的找出泄漏的真正原因, – 參考自《LeakCanary2.0決議》
看了下 Koom 分析參考鏈的程序,大致可以分為以下幾個步驟:
- 分析 hprof 檔案,獲取鏡像所有的 instance 實體
- 遍歷所有的實體,判斷這個實體與各個 Detectors 是否有存在泄漏,如果有,則記錄 objectId 到集合
- 根據 objectId 集合獲取各個泄漏實體參考鏈,分析出 gcRoot,并遍歷 gcRoot 下的參考路徑
這個地方重點在于如何找到泄漏的 objectId,因為找到 objectId,即可找到泄漏參考鏈,在分析 hprof 的時候我們可以拿到 dump 時的記憶體實體,那么,我們可以根據這個實體來判斷是否泄漏,例如:
- Activity : 判斷實體是否是 android.app.Activity 的子類,并且 mFinished 或 mDestroyed 是否為 true (Activity 關閉時該值會為 true),因為 Activity 不泄露的話肯定是會被釋放,所以,不可能存在于 dump 的實體中,有就是發生了泄漏
- Bitmap : 獲取實體的類名稱是否為 android.graphics.Bitmap,如果是的話,則獲取實體的 mWidth 和 mHeight 實體變數,計算兩者的乘積是否超過閾值,是的話,也判定為泄漏
- … (更多判斷可以看 analysis 目錄的各個 Detector)
Shark 根據 objectId 分析出的參考鏈路徑:
┬───
│ GC Root: Local variable in native code
│
├─ android.os.HandlerThread instance
│ Leaking: UNKNOWN
│ ↓ HandlerThread.contextClassLoader
│ ~~~~~~~~~~~~~~~~~~
├─ dalvik.system.PathClassLoader instance
│ Leaking: UNKNOWN
│ ↓ PathClassLoader.runtimeInternalObjects
│ ~~~~~~~~~~~~~~~~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ ↓ Object[].[197]
│ ~~~~~
├─ com.kwai.koom.demo.leaked.ActivityLeakMaker$LeakedActivity class
│ Leaking: UNKNOWN
│ ↓ static ActivityLeakMaker$LeakedActivity.uselessObjectList
│ ~~~~~~~~~~~~~~~~~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ ↓ Object[].[0]
│ ~~~
╰→ com.kwai.koom.demo.leaked.ActivityLeakMaker$LeakedActivity instance
? Leaking: YES (This is the leaking object), Signature: 39f4102649e5d3a5be12db591c2e5f68a1c0d2e9
三、如何應用于線上
1、解決 dump 凍結問題
由于 dump hprof 會暫停所有 java 執行緒問題,致使 LeakCanary 只能應用于線下檢測,但 Koom 和 Liko 另辟蹊徑,采用 linux 的 copy-on-write 機制,從當前的主執行緒 fork 出一個子行程,然后在子行程進行 dump 分析,對于用戶所在的行程不會有任何感知, ?
這個地方會有個坑,就是在 fork 子行程的時候 dump hprof,由于 dump 前會先 suspend 所有的 java 執行緒,等所有執行緒都掛起來了,才會進行真正的 dump,由于 copy-on-write 機制,子行程也會將父行程中的 threadList 也拷貝過來,但由于 threadList 中的 java 執行緒活動在父行程,子行程是無法掛起父行程中的執行緒的,然后就會一直處于等待中, ?
為了解決這個問題,Koom 和 Liko 采用欺騙的方式,在 fork 子行程之前,先將父行程中的 threadList 全部設定為 suspend 狀態,然后 fork 子行程,子行程在 dump 的時候發現 threadList 都為掛起狀態了,就立馬開始 dump hprof,然后父行程在 fork 操作之后,立馬 resume 恢復回 threadList 的狀態 ?
2、解決混淆問題
Shark 支持混淆反決議,思路也很簡單,決議 mapping.txt 檔案,每次讀取一行,只決議類和欄位:
- 類特征 :行尾為
:冒號結尾,然后根據->作為 index 分割,左邊的為原類名,右邊的為混淆類名 - 欄位特征:行尾不為
:冒號結尾,并且不包含(括號(帶括號的為方法),即為欄位特征,根據->作為 index 分割,左邊為原欄位名,右邊的為混淆欄位名
將混淆類名、欄位名作為 key,原類名、原欄位名作為 value 存入 map 集合,在分析出記憶體泄漏的參考路徑類時,將類名和欄位名都通過這個 map 集合去拿到原始類名和欄位名即可,即完成混淆后的反決議 ?
leakCanary 內部是寫死的 mapping 檔案為 leakCanaryObfuscationMapping.txt,如果打開該檔案失敗,則不做參考鏈反決議:

也即意味著,如果想 LeakCanary 支持混淆反決議,只需要將自己的 mapping 檔案重命名為 leakCanaryObfuscationMapping.txt,然后放入 asset 目錄即可
對于 Koom 的混淆反決議,Koom 并沒有做,但我們可以自己去加這塊代碼:
private boolean buildIndex() {
...
try {
// 新增 ---------- start
InputStream is = KGlobalConfig.getApplication().getResources().getAssets().open("mapping.txt");
ProguardMapping mapping = new ProguardMappingReader(is).readProguardMapping();
// 新增 ---------- end
heapGraph = HprofHeapGraph.Companion.indexHprof(hprof, mapping,
kotlin.collections.SetsKt.setOf(gcRoots));
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
將 mapping.txt 檔案放到 asset 目錄即可,如下是混淆與混淆反決議的參考鏈的對比:

3、泄漏兜底
在預判記憶體泄漏發生時,我們可以將 Activity 中參考到的 Bitmap、DrawingCache 等進行主動釋放,以此來降低泄漏的影響面,做法是,在 Activity onDestory 時候從 view 的 rootview 開始,遞回釋放所有子 view 涉及的圖片、背景、DrawingCache、監聽器等等資源,讓 Activity 成為一個不占資源的空殼,泄露了也不會導致圖片資源被持有,eg:
...
Drawable d = iv.getDrawable();
if (d != null) {
d.setCallback(null);
}
iv.setImageDrawable(null);
...
...
但這一點對于閾值檢測法的 Koom 來說,沒辦法做到,因為他拿不到 onDestroy 時的 Activity 實體,但也不要緊,我們可以將兜底操作做成通用操作,不管他泄漏與不泄露,都做 view 相關參考的卸載,
四、總結:
整體下來,分析個記憶體泄漏其實并不難,難就難在我們平時并沒有養成好的習慣,對于參考的傳遞考慮的不周全,但我們可以加強自身的編碼習慣,盡量減少專案中的泄漏問題
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/317874.html
標籤:其他
上一篇:資料轉發程序
