內存分析工具LeakCanary是如何工作的

一旦LeakCanary被安裝,它自動檢測和報告內存泄漏,分4步:

  1. 檢測保留下來的對象;
  2. 導出堆信息;
  3. 分析堆信息;
  4. 對內存泄漏進行分類;

目錄

 

1.檢測保留下來的對象

2.導出堆文件

3.分析堆文件

4.對內存泄漏進行分類

5.其他


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文件,並在通知欄顯示最新未銷燬對象的數量;

notification

通知提示有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,在導出堆文件時會有如下通知:

toast

默認存儲堆文件在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文件並定位在堆中無法回收被保留的對象;

done

以上是LeakCanary在堆堆文件找到被保留對象通知;

對於每個保留對象,LeakCanary都會找到防止該保留對象被垃圾回收的引用路徑:其泄漏跟蹤。下一節將學習分析泄漏跟蹤:修復內存泄漏

done

以上通知提示在計算被保留對象的引用路徑;

分析完成後,LeakCanary會顯示一個帶有摘要的通知,並在Logcat中打印結果。請注意下面4個保留對象如何分組爲2個不同的泄漏。LeakCanary爲每個泄漏跟蹤創建一個簽名,並將具有相同簽名的泄漏(即由相同錯誤引起的泄漏)組合在一起。

done

以上表示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表示第一次出現內存泄漏問題;

toast

以上表示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:

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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章