本篇文章借助了Google翻譯對square/leakcanary的官方文檔Getting started部分和Fundamentals部分進行了翻譯并加入了自己的理解。
LeakCanary版本:2.0-beta-4
Getting started
在app的build.gradle文件中添加依賴
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-4'
}
使用debugImplementation,因為LeakCanary只應該在調試版本中使用。
這就完了,使用老版本的LeakCanary的時候還需要在Application中在做一些初始化操作,現(xiàn)在完全不必要了。為啥呢?
在Android應用中,content providers在Application實例創(chuàng)建之后但是在Application的onCreate()方法調用之前被創(chuàng)建。leakcanary-object-watcher-android在其AndroidManifest.xml文件中定義了一個未公開的ContentProvider。
<provider
android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
android:authorities="${applicationId}.leakcanary-installer"
android:enabled="@bool/leak_canary_watcher_auto_install"
android:exported="false"/>
當安裝該ContentProvider后,在 MainProcessAppWatcherInstaller 的 onCreate() 方法中,
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
調用了 AppWatcher.manualInstall(application) 方法。它將向應用程序添加活動和片段生命周期偵聽器。
Fundamentals
什么是內存泄漏
在一個基于Java的運行環(huán)境中,內存泄漏是一個程序錯誤,該錯誤會導致應用保留不再需要的對象的引用。結果就是無法回收為該對象分配的內存,最終導致OutOfMemoryError崩潰。
內存泄漏的常見原因
大多數(shù)內存泄漏是由與對象生命周期相關的錯誤引起的。這里有幾個Android中常見的錯誤。
- 在一個對象中存儲Activity的Context作為成員變量,那么當Activity由于屏幕旋轉等配置改變導致Activity重新創(chuàng)建的時候,前一個Activity由于被持有而不能被回收。
- 注冊一個監(jiān)聽器,廣播接收器或者RxJava訂閱到一個具有生命周期的對象,但是當該對象生命周期結束的時候沒有取消訂閱導致該對象不能被回收。
- 在一個靜態(tài)成員變量中存儲一個View,但是在View
detached的時候沒有清除靜態(tài)成員變量(將該靜態(tài)成員變量賦值為null)。
為什么我應該使用LeakCanary
在Android應用中內存泄漏很常見,小的內存泄露不斷積累導致應用內存耗盡最終導致OutOfMemoryError。使用LeakCanary可以發(fā)現(xiàn)修復許多內存泄漏問題,降低OutOfMemoryError的發(fā)生概率。
LeakCanary是怎么工作的?
檢測保留的對象
LeakCanary的基礎是一個叫做ObjectWatcher Android的library。它hook了Android的生命周期,當activity和fragment 被銷毀并且應該被垃圾回收時候自動檢測。這些被銷毀的對象被傳遞給ObjectWatcher, ObjectWatcher持有這些被銷毀對象的弱引用(weak references)。你也可以觀察任何不再需要的對象,例如一個detached view, 一個銷毀的presenter等等。
AppWatcher.objectWatcher.watch(myDetachedView)
如果弱引用在等待5秒鐘并運行垃圾收集器后仍未被清除,那么被觀察的對象就被認為是保留的(retained,在生命周期結束后仍然保留),并存在潛在的泄漏。LeakCanary會在Logcat中輸出這些日志。
D LeakCanary: Watching instance of com.example.leakcanary.MainActivity
// 5 seconds later...
D LeakCanary: Found 1 retained object
LeakCanary在堆轉儲之前會等待保留的對象到達一個閾值,并且會顯示一個最新數(shù)量的一個通知。

注意:當應用可見的時候默認的閾值是5,應用不可見的時候閾值是1。如果你看到了保留的對象的通知然后將應用切換到后臺(例如點擊home鍵),那么閾值就會從5變到1,LeakCanary會立即進行堆轉儲。點擊通知也可以強制LeakCanary立即進行堆轉儲。
堆轉儲(Dumping the heap)
當保留的對象數(shù)量達到閾值以后,LeakCanary會將Java heap信息存儲到一個.hprof文件中,該文件存儲在在Android的文件系統(tǒng)中。該過程會凍結應用很短的一段時間,并顯示如下一個toast。

凍結的原因,看源碼是當前線程等待了5秒鐘。
@Override public File dumpHeap() {
File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
if (heapDumpFile == RETRY_LATER) {
return RETRY_LATER;
}
FutureResult<Toast> waitingForToast = new FutureResult<>();
showToast(waitingForToast);
//注釋1處,F(xiàn)utureResult的 wait 方法。
if (!waitingForToast.wait(5, SECONDS)) {
CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
return RETRY_LATER;
}
Toast toast = waitingForToast.get();
try {
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
cancelToast(toast);
return heapDumpFile;
} catch (Exception e) {
CanaryLog.d(e, "Could not dump heap");
// Abort heap dump
return RETRY_LATER;
}
}
注釋1處,F(xiàn)utureResult的 wait 方法。
public final class FutureResult<T> {
private final AtomicReference<T> resultHolder;
private final CountDownLatch latch;
public FutureResult() {
resultHolder = new AtomicReference<>();
latch = new CountDownLatch(1);
}
public boolean wait(long timeout, TimeUnit unit) {
try {
return latch.await(timeout, unit);
} catch (InterruptedException e) {
throw new RuntimeException("Did not expect thread to be interrupted", e);
}
}
//...
}
分析堆信息
LeakCanary使用shark來轉換.hprof文件并定位Java堆中保留的對象。如果找不到保留的對象,那么它們很可能在堆轉儲的過程中被回收了。

對于每個被保留的對象,LeakCanary會找出阻止該保留對象被回收的引用鏈:泄漏路徑。泄露路徑就是從GC ROOTS到保留對象的最短的強引用路徑的別名。確定泄漏路徑以后,LeakCanary使用它對Android框架的了解來找出在泄漏路徑上是誰泄漏了。
當分析完畢以后,LeakCanary會顯示一個通知,點擊通知可以查看分析結果。

如何修復內存泄漏?
對于每個泄漏的對象,LeakCanary計算一個泄漏路徑并在UI上展示出來。

泄漏路徑也會在Logcat中輸出:
┬
├─ leakcanary.internal.InternalLeakCanary
│ Leaking: NO (it's a GC root and a class is never leaking)
│ ↓ static InternalLeakCanary.application
├─ com.example.leakcanary.ExampleApplication
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList
│ Leaking: UNKNOWN
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[]
│ Leaking: UNKNOWN
│ ↓ array Object[].[0]
│ ~~~
├─ android.widget.TextView
│ Leaking: YES (View detached and has parent)
│ View#mAttachInfo is null (view detached)
│ View#mParent is set
│ View.mWindowAttachCount=1
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity
Leaking: YES (RefWatcher was watching this and MainActivity#mDestroyed
is true)
對象和引用
├─ android.widget.TextView
泄漏路徑中的每個節(jié)點是一個Java對象。對象的類型可能是一個class對象,一個對象數(shù)組或者一個普通的對象。
│ ↓ TextView.mContext
從GC ROOTS向下,每個節(jié)點都有對下一個節(jié)點的引用。在UI上,引用是紫色的。在Logcat中,引用在以向下箭頭開頭的行上。
GC Root
┬
├─ leakcanary.internal.InternalLeakCanary
│ Leaking: NO (it's a GC root and a class is never leaking)
在泄漏路徑的頂部是GC Root。GC Root是一些總是可達的特殊對象。這里有四種GC Root值得一提:
- 局部變量(Local variables),屬于線程棧中的變量。
- 活動的Java線程實例。
- 類(Class)對象,永遠不會再Android上卸載。
- 本地引用(Native references),由本地代碼控制。
泄漏的對象
╰→ com.example.leakcanary.MainActivity
Leaking: YES (RefWatcher was watching this and MainActivity#mDestroyed
is true)
在泄漏路徑的底部是泄漏的對象。該對象已傳遞給AppWatcher.objectWatcher以確認將被垃圾回收,并且最終沒有被垃圾回收,從而觸發(fā)了LeakCanary。
引用鏈
...
│ ↓ static InternalLeakCanary.application
...
│ ↓ ExampleApplication.leakedViews
...
│ ↓ ArrayList.elementData
...
│ ↓ array Object[].[0]
...
│ ↓ TextView.mContext
...
從GC ROOTS到泄漏對象之間的引用鏈阻止了泄漏對象被垃圾回收。如果你可以確定某個引用在某個時間點不應該存在,那么你可以弄清楚為什么它仍然存在并修復內存泄漏。
啟發(fā)式和標簽(Heuristics and labels)
├─ android.widget.TextView
│ Leaking: YES (View detached and has parent)
│ View#mAttachInfo is null (view detached)
│ View#mParent is set
│ View.mWindowAttachCount=1
LeakCanary使用啟發(fā)式的方式來確定泄漏路徑上的節(jié)點的生命周期狀態(tài),從而確定它們是否泄漏。例如,如果一個View顯示View#mAttachInfo = null和mParent != null,那么這個View就是處于View detached and has parent的狀態(tài),那么這個View可能泄漏了。在泄漏路徑上,每一個節(jié)點都會有一個Leaking狀態(tài)Leaking: YES / NO / UNKNOWN并在括號中解釋為什么這個節(jié)點泄漏了。LeakCanary還可以顯示有關節(jié)點狀態(tài)的額外信息,例如View.mWindowAttachCount=1。LeakCanary帶有一組默認啟發(fā)式方法AndroidObjectInspectors。你可以添加你自己的啟發(fā)式方法通過更改LeakCanary.Config.objectInspectors。
疑問:啥是啟發(fā)式?在好多地方都看到heuristics這個單詞。
縮小泄漏原因
┬
├─ android.provider.FontsContract
│ Leaking: NO (ExampleApplication↓ is not leaking and a class is never leaking)
│ GC Root: System class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication
│ Leaking: NO (Application is a singleton)
│ ExampleApplication does not wrap an activity context
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList
│ Leaking: UNKNOWN
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[]
│ Leaking: UNKNOWN
│ ↓ array Object[].[1]
│ ~~~
├─ android.widget.TextView
│ Leaking: YES (View.mContext references a destroyed activity)
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity
Leaking: YES (TextView↑ is leaking and Activity#mDestroyed is true and ObjectWatcher was watching this)
如果一個節(jié)點沒有泄漏,那么指向該節(jié)點的任何先前的引用都不是泄漏的來源,也不會泄漏。相似的,如果一個節(jié)點泄漏了,那么該節(jié)點下面的所有節(jié)點也泄漏了。據(jù)此,我們可以推斷泄漏是由最后一個沒有泄漏的節(jié)點(Leaking: NO )和第一個泄漏的節(jié)點(Leaking: YES)之間的引用導致的。
LeakCanary使用在UI上使用紅色下劃線標記這些引用,在在Logcat中使用 ~~~~ 標記。這些被標記的引用只可能是造成泄漏的原因。這些引用你應該花時間來調查。
在這個例子中,最后一個沒有泄漏(Leaking: NO)的節(jié)點是com.example.leakcanary.ExampleApplication,第一個泄漏的節(jié)點是android.widget.TextView。所以泄漏是由這兩個節(jié)點之間的三個引用之一導致的。
...
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
...
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
...
│ ↓ array Object[].[0]
│ ~~~
...
查看源代碼可以看到ExampleApplication有一個列表成員變量:
open class ExampleApplication : Application() {
val leakedViews = mutableListOf<View>()
}
由于ArrayList自身實現(xiàn)的bug導致內存泄漏是不太可能的,所以泄漏發(fā)生是因為我們向ExampleApplication.leakedViews中添加View。如果我們停止向ExampleApplication.leakedViews中添加View,那么我們就解決了泄漏問題。
參考鏈接