利用 LeakCanary 來檢查 Android 內(nèi)存泄漏

你被概率性的 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

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)存泄漏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容