檢測內存是否泄漏非常簡單,只要在任意位置調用 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 的檢測預判 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) ?
閾值檢測法的代表框架是 Koom,他拋棄了 LeakCanary 的實時檢測性,采用定時輪訓檢測當前內存是否在不斷累加,增長達到一定次數(可自己配置)時會進行 dump hprof,這種方式會犧牲一定的時效性,但對于應用到線上的 Koom 的框架,他完全不需要這么高的時效性 ?
分析工具代表:
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 實例導致:
Android Studio 的 Profiler 工具支持 hprof 的解析,并且很智能的提示當前 leak 了哪些對象,打開方式很簡單,將 hprof 文件拖拽至 as,然后雙擊 hprof 文件即可:
我們可以很直觀的看到,當前 LeakedActivity 和 ReportFragment 發生了泄漏。 ?
如果我們的需求僅僅只是在開發階段進行內存泄漏檢測的話,并且又不想接入 LeakCanary(因為有時候想調試下自己模塊的代碼,其他模塊經常報內存泄漏,凍結當前線程,很影響調試),那么我們可以在應用里面埋個彩蛋,比如單擊 5 次版本號,然后調用 Debug.dumpHprofData ,然后將 hprof 文件導出到 as 進行分析,這就將原本可能會進行數次 dump 的過程,改成了自己需要去檢測的時候再去 dump。 ?
在 LeakCanary 的第一版的時候,是采用的 Haha 庫來分析泄漏引用鏈,但由于后面新出的 Shark,比 HaHa 快 8 倍,并且內存占用還要少 10 倍,但查找泄漏路徑的大致步驟與 Shark 無異,故此文就不分析 HaHa 了。 ?
Shark 是 square 團隊開發的一款全新的分析 hprof 文件的工具,其官方宣布比 Android Studio 用于 memory profiler 的核心庫 perflib 要快 8 倍并且內存占用少 10 倍,更加適合手機端的分析工具。其目的就是提供快速解析hprof文件和分析快照的能力,并找出真正的泄漏對象以及對象到GcRoot 的最短引用路徑鏈,以便幫助開發者更加直觀的找出泄漏的真正原因。 – 引用自《LeakCanary2.0解析》
看了下 Koom 分析引用鏈的過程,大致可以分為以下幾個步驟:
這個地方重點在于如何找到泄漏的 objectId,因為找到 objectId,即可找到泄漏引用鏈。在分析 hprof 的時候我們可以拿到 dump 時的內存實例,那么,我們可以根據這個實例來判斷是否泄漏,例如:
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
由于 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 的狀態 ?
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 目錄即可,如下是混淆與混淆反解析的引用鏈的對比:
在預判內存泄漏發生時,我們可以將 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 相關引用的卸載。
整體下來,分析個內存泄漏其實并不難,難就難在我們平時并沒有養成好的習慣,對于引用的傳遞考慮的不周全,但我們可以加強自身的編碼習慣,盡量減少項目中的泄漏問題
|