利用 LeakCanary 来检查 Android 内存泄漏

你被概率性的 OOM 困扰么?有时候,OOM 像幽灵一样,挥之不去,可真想把它揪出来时,又捉之不着。或许,是时候用 LeakCanary 来诊断一下了。它是一个用来检查 Android 下内存泄漏的开源库,这篇文章主要介绍其用法、架构和其背后的实现原理。
Square 有篇文章介绍了开发这个库的原因。他们的一个付款流程里,需要用到用户的签名,他们直接用 Bitmap 来画签名,Bitmap 大小和屏幕分辨率是一样的。问题来了,在试图创建这个 Bitmap 对象时,概率性 OOM 如幽灵般相随。他们试了几个方法:
使用 Bitmap.Config.ALPHA_8
来节省内存
捕获 OutOfMemoryError
异常,调用 gc 清理内存,然后重试几次

最终这些都不起作用。最终他们发现他们在错误的方向上走得太远了。当存在内存泄漏时,可用内存越来越少,这个时候 OOM 可以发生在任何地方,特别是试图创建一些大内存对象,如 Bitmap 的时候。
我们在上一篇文章《Android 内存与性能》里介绍了使用 MAT 来分析内存泄漏的方法。概括起来核心步骤是:
发生 OOM 或做一些可能存在内存泄漏的操作后,导出 HPROF 文件
利用 MAT 结合代码分析,来发现一些引用异常,比如哪些对象本来应该被回收的,却还在系统堆中,那么它就是内存泄漏

如果有一个工具能自动完成这些事情,甚至在发生 OOM 之前,就把内存泄漏报告给你,那是多么美好的一件事情啊。LeakCanary 就是用来干这个事情的。在测试你的 App 时,如果发生了内存泄漏,状态栏上会有通知告诉你。logcat 上也会有相应的 log 通知你。
启发
LeakCanary 产生的背后有几个有意思的启发。一是像 Square 这样公司一样会被 OOM 这种问题困扰;二是他们也会犯错,试了几种方法都不起作用;三是他们最终用一个优雅的方式解决了这个问题,并且通过开源库的方式让所有人共享他们的工作成果。

用法
监控 Activity 泄露
我们经常把 Activity 当作为 Context 对象使用,在不同场合由各种对象引用 Activity。所以,Activity 泄漏是一个重要的需要检查的内存泄漏之一。

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()
返回一个配置好了的 RefWatcher
实例。它同时安装了 ActivityRefWatcher
来监控 Activity 泄漏。即当 Activity.onDestroy()
被调用之后,如果这个 Activity 没有被销毁,logcat 就会打印出如下信息告诉你内存泄漏发生了。

 * 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 自动检测 Activity 泄漏只支持 android ICS 以上版本。因为 Application.registerActivityLifecycleCallbacks()
是在 API 14 引入的。如果要在 ICS 之前监测 Activity 泄漏,可以重载 Activity.onDestroy()
方法,然后在这个方法里调用 RefWatcher.watch(this)
来实现。

监控 Fragment 泄漏

public abstract class BaseFragment extends Fragment {

    @Override 
    public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
        refWatcher.watch(this);
    }
}

当 Fragment.onDestroy()
被调用之后,如果这个 fragment 实例没有被销毁,那么就会从 logcat 里看到相应的泄漏信息。
监控其他泄漏

    ...
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(someObjNeedGced);

当 someObjNeedGced
还在内存中时,就会在 logcat 里看到内存泄漏的提示。
集成 LeakCanary 库

dependencies {
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}

在 debug 版本上,集成 LeakCanary 库,并执行内存泄漏监测,而在 release 版本上,集成一个无操作的 wrapper ,这样对程序性能就不会有影响。
原理
LeakCanary 流程图


leakcanary

LeakCanary 的机制如下:

RefWatcher.watch()会以监控对象来创建一个 KeyedWeakReference弱引用对象
在 AndroidWatchExecutor的后台线程里,来检查弱引用已经被清除了,如果没被清除,则执行一次 GC
如果弱引用对象仍然没有被清除,说明内存泄漏了,系统就导出 hprof 文件,保存在 app 的文件系统目录下
HeapAnalyzerService
 启动一个单独的进程,使用 HeapAnalyzer
 来分析 hprof 文件。它使用另外一个开源库 [HAHA](https://github.com/square/haha)。
HeapAnalyzer
 通过查找 KeyedWeakReference
 弱引用对象来查找内在泄漏
HeapAnalyzer
 计算 KeyedWeakReference
 所引用对象的最短强引用路径,来分析内存泄漏,并且构建出对象引用链出来。
内存泄漏信息送回给 DisplayLeakService
,它是运行在 app 进程里的一个服务。然后在设备通知栏显示内存泄漏信息。

几个有意思的代码
如何导出 hprof 文件

File heapDumpFile = new File("heapdump.hprof");
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());

可以参阅 AndroidHeapDumper.java 的代码。
如何分析 hprof 文件
这是个比较大的话题,感兴趣的可以移步另外一个开源库 HAHA,它的祖先是 MAT
如何使用 HandlerThread
可以参阅 AndroidWatchExecutor.java的代码,特别是关于 Handler, Loop 的使用。
怎么知道某个变量已经被 GC 回收
可以参阅 RefWatcher.java 的 ensureGone()
函数。最主要是利用 WeakReference
和 ReferenceQueue
机制。简单地讲,就是当弱引用 WeakReference
所引用的对象被回收后,这个 WeakReference
对象就会被添加到 ReferenceQueue
队列里,我们可以通过其 poll()
方法获取到这个被回收的对象的 WeakReference
实例,进而知道需要监控的对象是否被回收了。
关于内存泄漏
内存泄漏可能很容易发现,比如 Cursor 没关闭;比如在 Activity.onResume()
里 register 了某个需要监听的事件,但在 Activity.onPause()
里忘记 unregister 了;内存泄漏也可能很难发现,比如 LeakCanary 示例代码,隐含地引用,并且只有在旋转屏幕时才会发生。还有更难发现,甚至无能为力的内存泄漏,比如 Android SDK 本身的 BUG 导致内存泄漏。AndroidExcludedRefs.java 里就记录了一些己知的 AOSP 版本的以及其 OEM 实现版本里存在的内存泄漏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容