你被概率性的 OOM 困擾么?有時(shí)候,OOM 像幽靈一樣,揮之不去,可真想把它揪出來時(shí),又捉之不著。或許,是時(shí)候用 LeakCanary 來診斷一下了。它是一個(gè)用來檢查 Android 下內(nèi)存泄漏的開源庫,這篇文章主要介紹其用法、架構(gòu)和其背后的實(shí)現(xiàn)原理。
Square 有篇文章介紹了開發(fā)這個(gè)庫的原因。他們的一個(gè)付款流程里,需要用到用戶的簽名,他們直接用 Bitmap 來畫簽名,Bitmap 大小和屏幕分辨率是一樣的。問題來了,在試圖創(chuàng)建這個(gè) Bitmap 對(duì)象時(shí),概率性 OOM 如幽靈般相隨。他們試了幾個(gè)方法:
使用 Bitmap.Config.ALPHA_8
來節(jié)省內(nèi)存
捕獲 OutOfMemoryError
異常,調(diào)用 gc 清理內(nèi)存,然后重試幾次
最終這些都不起作用。最終他們發(fā)現(xiàn)他們在錯(cuò)誤的方向上走得太遠(yuǎn)了。當(dāng)存在內(nèi)存泄漏時(shí),可用內(nèi)存越來越少,這個(gè)時(shí)候 OOM 可以發(fā)生在任何地方,特別是試圖創(chuàng)建一些大內(nèi)存對(duì)象,如 Bitmap 的時(shí)候。
我們在上一篇文章《Android 內(nèi)存與性能》里介紹了使用 MAT 來分析內(nèi)存泄漏的方法。概括起來核心步驟是:
發(fā)生 OOM 或做一些可能存在內(nèi)存泄漏的操作后,導(dǎo)出 HPROF 文件
利用 MAT 結(jié)合代碼分析,來發(fā)現(xiàn)一些引用異常,比如哪些對(duì)象本來應(yīng)該被回收的,卻還在系統(tǒng)堆中,那么它就是內(nèi)存泄漏
如果有一個(gè)工具能自動(dòng)完成這些事情,甚至在發(fā)生 OOM 之前,就把內(nèi)存泄漏報(bào)告給你,那是多么美好的一件事情啊。LeakCanary 就是用來干這個(gè)事情的。在測試你的 App 時(shí),如果發(fā)生了內(nèi)存泄漏,狀態(tài)欄上會(huì)有通知告訴你。logcat 上也會(huì)有相應(yīng)的 log 通知你。
啟發(fā)
LeakCanary 產(chǎn)生的背后有幾個(gè)有意思的啟發(fā)。一是像 Square 這樣公司一樣會(huì)被 OOM 這種問題困擾;二是他們也會(huì)犯錯(cuò),試了幾種方法都不起作用;三是他們最終用一個(gè)優(yōu)雅的方式解決了這個(gè)問題,并且通過開源庫的方式讓所有人共享他們的工作成果。
用法
監(jiān)控 Activity 泄露
我們經(jīng)常把 Activity 當(dāng)作為 Context 對(duì)象使用,在不同場合由各種對(duì)象引用 Activity。所以,Activity 泄漏是一個(gè)重要的需要檢查的內(nèi)存泄漏之一。
public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
private RefWatcher refWatcher;
@Override public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
}
LeakCanary.install()
返回一個(gè)配置好了的 RefWatcher
實(shí)例。它同時(shí)安裝了 ActivityRefWatcher
來監(jiān)控 Activity 泄漏。即當(dāng) Activity.onDestroy()
被調(diào)用之后,如果這個(gè) Activity 沒有被銷毀,logcat 就會(huì)打印出如下信息告訴你內(nèi)存泄漏發(fā)生了。
* com.example.leakcanary.MainActivity has leaked:
* GC ROOT thread java.lang.Thread.<Java Local> (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$2.this$0 (anonymous class extends android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance
* Reference Key: c4d32914-618d-4caf-993b-4b835c255873
* Device: Genymotion generic Google Galaxy Nexus - 4.2.2 - API 17 - 720x1280 vbox86p
* Android Version: 4.2.2 API: 17
* Durations: watch=5100ms, gc=104ms, heap dump=82ms, analysis=3008ms
Notes
LeakCanary 自動(dòng)檢測 Activity 泄漏只支持 android ICS 以上版本。因?yàn)?Application.registerActivityLifecycleCallbacks()
是在 API 14 引入的。如果要在 ICS 之前監(jiān)測 Activity 泄漏,可以重載 Activity.onDestroy()
方法,然后在這個(gè)方法里調(diào)用 RefWatcher.watch(this)
來實(shí)現(xiàn)。
監(jiān)控 Fragment 泄漏
public abstract class BaseFragment extends Fragment {
@Override
public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}
當(dāng) Fragment.onDestroy()
被調(diào)用之后,如果這個(gè) fragment 實(shí)例沒有被銷毀,那么就會(huì)從 logcat 里看到相應(yīng)的泄漏信息。
監(jiān)控其他泄漏
...
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(someObjNeedGced);
當(dāng) someObjNeedGced
還在內(nèi)存中時(shí),就會(huì)在 logcat 里看到內(nèi)存泄漏的提示。
集成 LeakCanary 庫
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
在 debug 版本上,集成 LeakCanary 庫,并執(zhí)行內(nèi)存泄漏監(jiān)測,而在 release 版本上,集成一個(gè)無操作的 wrapper ,這樣對(duì)程序性能就不會(huì)有影響。
原理
LeakCanary 流程圖

LeakCanary 的機(jī)制如下:
RefWatcher.watch()會(huì)以監(jiān)控對(duì)象來創(chuàng)建一個(gè) KeyedWeakReference弱引用對(duì)象
在 AndroidWatchExecutor的后臺(tái)線程里,來檢查弱引用已經(jīng)被清除了,如果沒被清除,則執(zhí)行一次 GC
如果弱引用對(duì)象仍然沒有被清除,說明內(nèi)存泄漏了,系統(tǒng)就導(dǎo)出 hprof 文件,保存在 app 的文件系統(tǒng)目錄下
HeapAnalyzerService
啟動(dòng)一個(gè)單獨(dú)的進(jìn)程,使用 HeapAnalyzer
來分析 hprof 文件。它使用另外一個(gè)開源庫 [HAHA](https://github.com/square/haha)。
HeapAnalyzer
通過查找 KeyedWeakReference
弱引用對(duì)象來查找內(nèi)在泄漏
HeapAnalyzer
計(jì)算 KeyedWeakReference
所引用對(duì)象的最短強(qiáng)引用路徑,來分析內(nèi)存泄漏,并且構(gòu)建出對(duì)象引用鏈出來。
內(nèi)存泄漏信息送回給 DisplayLeakService
,它是運(yùn)行在 app 進(jìn)程里的一個(gè)服務(wù)。然后在設(shè)備通知欄顯示內(nèi)存泄漏信息。
幾個(gè)有意思的代碼
如何導(dǎo)出 hprof 文件
File heapDumpFile = new File("heapdump.hprof");
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
可以參閱 AndroidHeapDumper.java 的代碼。
如何分析 hprof 文件
這是個(gè)比較大的話題,感興趣的可以移步另外一個(gè)開源庫 HAHA,它的祖先是 MAT。
如何使用 HandlerThread
可以參閱 AndroidWatchExecutor.java的代碼,特別是關(guān)于 Handler, Loop 的使用。
怎么知道某個(gè)變量已經(jīng)被 GC 回收
可以參閱 RefWatcher.java 的 ensureGone()
函數(shù)。最主要是利用 WeakReference
和 ReferenceQueue
機(jī)制。簡單地講,就是當(dāng)弱引用 WeakReference
所引用的對(duì)象被回收后,這個(gè) WeakReference
對(duì)象就會(huì)被添加到 ReferenceQueue
隊(duì)列里,我們可以通過其 poll()
方法獲取到這個(gè)被回收的對(duì)象的 WeakReference
實(shí)例,進(jìn)而知道需要監(jiān)控的對(duì)象是否被回收了。
關(guān)于內(nèi)存泄漏
內(nèi)存泄漏可能很容易發(fā)現(xiàn),比如 Cursor 沒關(guān)閉;比如在 Activity.onResume()
里 register 了某個(gè)需要監(jiān)聽的事件,但在 Activity.onPause()
里忘記 unregister 了;內(nèi)存泄漏也可能很難發(fā)現(xiàn),比如 LeakCanary 示例代碼,隱含地引用,并且只有在旋轉(zhuǎn)屏幕時(shí)才會(huì)發(fā)生。還有更難發(fā)現(xiàn),甚至無能為力的內(nèi)存泄漏,比如 Android SDK 本身的 BUG 導(dǎo)致內(nèi)存泄漏。AndroidExcludedRefs.java 里就記錄了一些己知的 AOSP 版本的以及其 OEM 實(shí)現(xiàn)版本里存在的內(nèi)存泄漏。