一旦LeakCanary被安裝,它自動檢測和報告內存泄漏,分4步:
- 檢測保留下來的對象;
- 導出堆信息;
- 分析堆信息;
- 對內存泄漏進行分類;
目錄
1.檢測保留下來的對象
LeakCanary通過Hook(劫持)Android生命週期去自動檢測內存泄漏問題,當Activity和Fragment被銷燬並且執行垃圾回收的時候;這些被銷燬的對象被傳遞給ObjectWatcher(持有這些銷燬的對象的弱引用);LeakCanary自動檢測如下對象的內存泄漏:
a.已銷燬的Activity實例;
b.已銷燬的Fragment實例;
c.已銷燬的片段View實例;
d.已經清除的ViewModel實例;
可以檢測任何不在需要的對象,例如一個被移除的View或者一個銷燬的Presenter:
AppWatcher.INSTANCE.getObjectWatcher().watch(textView2, "View was detached");
如果持有弱引用銷燬對象的ObjectWatcher在等候5秒並且運行垃圾回收不能被清除,被觀察的銷燬對象可能被保留,存在潛在的內存泄漏問題;
LeakCanary輸入日誌在Logcat控制檯下:
D LeakCanary: Watching instance of com.example.leakcanary.MainActivity
(Activity received Activity#onDestroy() callback)
... 5 seconds later ...
D LeakCanary: Scheduling check for retained objects because found new object
retained
LeakCanary一直等候被保留未銷燬的對象數量達到閥值(5)再導出heap堆hprof文件,並在通知欄顯示最新未銷燬對象的數量;
通知提示有4個未銷燬的對象被保留,點擊通知可以導出heap堆文件;
D LeakCanary: Rescheduling check for retained objects in 2000ms because found
only 4 retained objects (< 5 while app visible)
注意:
默認閾值爲應用程序可見時5個保留對象,應用程序不可見時1個保留對象。如果您看到retained objects通知,然後將應用程序置於後臺(例如按Home按鈕),那麼閾值將從5更改爲1,LeakCanary將在5秒內導出堆文件。點擊通知會強制LeakCanary立即導出堆文件。
2.導出堆文件
當未銷燬對象被保留達到閥值,LeakCanary導出Java的堆信息存儲到hprof文件;導出heap堆文件會短暫凍結APP,在導出堆文件時會有如下通知:
默認存儲堆文件在app文件夾下的leakcanary目錄下,如果設置android.permission.WRITE_EXTERNAL_STORAGE權限並授權此權限,則堆文件存儲在SD卡的Download/leakcanary-com.example目錄下,com.example是app的包名;
3.分析堆文件
Shark: Smart Heap Analysis Reports for Kotlin;
Shark是爲LeakCanary 2提供功能強大的堆分析器。它是一個Kotlin獨立堆分析庫,以低內存佔用率高速運行。
Shark被支持如下功能:
a.Shark Hprof:讀取和寫入Hprof文件中的記錄。
b.Shark Graph:導航堆對象圖。
c.Shark:生成堆分析報告。
d.Shark Android:Android啓發式生成定製的堆分析報告。
e.Shark CLI:分析安裝在連接到桌面的Android設備上的可調試應用程序堆。輸出與LeakCanary的輸出類似,只是您不必將LeakCanary依賴項添加到應用程序中。
LeakCanary:建在上面。它會自動監視被銷燬的Activity和Fragment,觸發堆存儲,運行Shark Android,然後顯示結果。
06-27 15:19:38.515 9186-9224/fan.fragmentdemo D/LeakCanary: Removing 1 heap dumps
06-27 15:19:41.523 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: PARSING_HEAP_DUMP
06-27 15:19:43.267 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: EXTRACTING_METADATA
06-27 15:19:43.469 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: FINDING_RETAINED_OBJECTS
06-27 15:19:43.994 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: FINDING_PATHS_TO_RETAINED_OBJECTS
06-27 15:19:44.500 9186-9223/fan.fragmentdemo D/LeakCanary: Setting up flushing for Thread[IntentService[HeapAnalyzerService],5,main]
06-27 15:19:47.737 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: FINDING_DOMINATORS
06-27 15:19:54.663 9186-9494/fan.fragmentdemo D/LeakCanary: Found 2 retained objects
06-27 15:19:54.663 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: COMPUTING_NATIVE_RETAINED_SIZE
06-27 15:19:55.480 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: COMPUTING_RETAINED_SIZE
06-27 15:19:55.554 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: BUILDING_LEAK_TRACES
06-27 15:19:55.558 9186-9494/fan.fragmentdemo D/LeakCanary: Found 2 paths to retained objects, down to 1 after removing duplicated paths
06-27 15:19:55.720 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: REPORTING_HEAP_ANALYSIS
06-27 15:19:55.737 9186-9494/fan.fragmentdemo D/LeakCanary: ====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
30451 bytes retained by leaking objects
Signature: f3466687f84b8cdd14a9862dcc5b72a7115e352b
┬───
│ GC Root: System class
│
├─ fan.fragmentdemo.MemoryTestActivity class
│ Leaking: NO (a class is never leaking)
│ ↓ static MemoryTestActivity.textView2
│ ~~~~~~~~~
╰→ android.support.v7.widget.AppCompatTextView instance
Leaking: YES (ObjectWatcher was watching this because View was detached and View.mContext references a destroyed activity)
key = 0f1c40a8-d5be-4253-ab5c-fdec9e64c65d
watchDurationMillis = 15965
retainedDurationMillis = 10964
mContext instance of fan.fragmentdemo.MemoryTestActivity with mDestroyed = true
View#mParent is set
View#mAttachInfo is null (view detached)
View.mID = R.id.textView2
View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS
A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
Build.VERSION.SDK_INT: 25
Build.MANUFACTURER: smartisan
LeakCanary version: 2.4
App process name: fan.fragmentdemo
Analysis duration: 14196 ms
Heap dump file path: /storage/emulated/0/Download/leakcanary-fan.fragmentdemo/2020-06-27_15-19-38_530.hprof
Heap dump timestamp: 1593242395719
====================================
以上是分析hprof文件的日誌;
LeakCanary通過Shark解析hprof文件並定位在堆中無法回收被保留的對象;
以上是LeakCanary在堆堆文件找到被保留對象通知;
對於每個保留對象,LeakCanary都會找到防止該保留對象被垃圾回收的引用路徑:其泄漏跟蹤。下一節將學習分析泄漏跟蹤:修復內存泄漏。
以上通知提示在計算被保留對象的引用路徑;
分析完成後,LeakCanary會顯示一個帶有摘要的通知,並在Logcat中打印結果。請注意下面4個保留對象如何分組爲2個不同的泄漏。LeakCanary爲每個泄漏跟蹤創建一個簽名,並將具有相同簽名的泄漏(即由相同錯誤引起的泄漏)組合在一起。
以上表示4個引用路徑分爲兩種不同的泄漏簽名;
====================================
HEAP ANALYSIS RESULT
====================================
2 APPLICATION LEAKS
Displaying only 1 leak trace out of 2 with the same signature
Signature: ce9dee3a1feb859fd3b3a9ff51e3ddfd8efbc6
┬───
│ GC Root: Local variable in native code
│
...
點擊通知可以打開Activity查看更詳細的泄漏問題,關閉Activity可以看到LeakCanary加載圖標:
以上表示增加了一個爲了每個被安裝的app增加了一個加載圖標;
每一行顯示一組有詳情簽名的內存泄漏問題;LeakCanary標記了一行New表示第一次出現內存泄漏問題;
以上表示4內存泄漏問題分在兩行,每行有不同的泄漏簽名;
點擊打開帶有泄漏引用路徑。您可以通過下拉菜單在保留對象及其泄漏引用路徑之間切換。
以上表示相同泄漏簽名的3個內存泄漏問題;
泄漏簽名是每個可能導致泄漏的引用的串聯的哈希值,即每個引用都用紅色下劃線顯示:
以上引用路徑存在三個子引用;
當泄漏的路徑被分享做爲文本時這些相同的子引用將有下劃線~~~
...
│
├─ com.example.leakcanary.LeakingSingleton class
│ Leaking: NO (a class is never leaking)
│ ↓ static LeakingSingleton.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ Leaking: YES (View.mContext references a destroyed activity)
...
以上的例子,泄漏的簽名將按照如下的方式計算:
val leakSignature = sha1Hash(
"com.example.leakcanary.LeakingSingleton.leakedView" +
"java.util.ArrayList.elementData" +
"java.lang.Object[].[x]"
)
println(leakSignature)
// dbfa277d7e5624792e8b60bc950cd164190a11aa
4.對內存泄漏進行分類
LeakCanary在你的app中分兩類,Applications Leaks和Library Leaks;一個Library Leak是被第三方庫引起的問題(超出你控制範圍的);這個leak泄漏影響的應用程序,因爲修改它可能不在你控制範圍因此LeakCanary把它分開說明;
這兩類被分開在Logcat控制檯打印:
====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code
│
...
LeakCanary標記一行在leaks列表中做爲Library Leak:
以上表示LeakCanary發現了一個庫的內存泄漏問題;
LeakCanary提供了一個已知泄漏的數據庫,它通過對引用名稱進行模式匹配來識別這些泄漏。例如:
Leak pattern: instance field android.app.Activity$1#this$0
Description: Android Q added a new IRequestFinishCallback$Stub class [...]
┬───
│ GC Root: Global variable in native code
│
├─ android.app.Activity$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of android.app.IRequestFinishCallback$Stub
│ ↓ Activity$1.this$0
│ ~~~~~~
╰→ com.example.MainActivity instance
5.其他
我做了什麼引起內存泄漏問題?
沒問題!您按照預期的方式使用了一個API,但實現中有一個導致此泄漏的bug。
如果阻止內存泄漏問題?
可能!一些庫泄漏可以使用反射修復,另一些可以通過使用使泄漏消失的代碼路徑修復。這種類型的修復往往是黑客,所以小心!您最好的選擇可能是找到bug報告或文件,並堅持bug得到修復。
既然我對這次泄漏無能爲力,有沒有辦法讓LeakCanary置之不理呢?
在堆文件對其進行分析之前,LeakCanary無法知道泄漏是否是庫泄漏。如果在發現庫泄漏時,LeakCanary沒有顯示結果通知,那麼您將開始懷疑在toast之後,LeakCanary分析發生了什麼。
參考 :
https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/