4、LeakCanary实战:集成配置、自定义检测规则、源码原理剖析
内存泄漏检测工具里,LeakCanary 绝对是绕不开的存在。
我个人习惯把它叫做「内存泄漏的照妖镜」。你想想看,以前排查内存泄漏,得靠 MAT 或者 Android Profiler 手动抓取 heap dump,然后对着几百兆的文件一点点分析。说实话,效率很低。LeakCanary 的出现,彻底改变了这个局面——它自动检测、自动分析、自动通知,一条龙服务。
4.1 集成配置:三分钟上手
集成 LeakCanary 其实很简单。但我要提醒你一点:千万别在 Release 包集成。这东西只适合 Debug 阶段用。
在 build.gradle 里加上依赖:
dependencies {
// Debug 版本专用
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
// Release 版本用空实现,避免 crash
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.12'
}
嗯,这里要注意:leakcanary-android-no-op 是一个空壳库,所有方法都是空实现。这样 Release 包不会引入任何额外开销。
集成完成后,你只需要在 Application 里初始化:
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
// 如果是 Debug 构建,LeakCanary 会自动初始化
// 但如果你想手动控制,可以这样写
if (LeakCanary.isInAnalyzerProcess(this)) {
// 这个进程是 LeakCanary 的分析进程
// 不要在这里做太多事情
return;
}
LeakCanary.install(this);
}
}
我曾经遇到过一个问题:LeakCanary 初始化后,App 启动变慢了。后来发现是因为我在主进程里做了太多初始化操作。记住,
isInAnalyzerProcess() 返回 true 时,那个进程是专门用来分析 heap dump 的,不要在里面做业务初始化。
4.2 自定义检测规则:让工具更懂你的业务
LeakCanary 默认会检测 Activity、Fragment、ViewModel 等常见对象的泄漏。但实际项目中,有些对象泄漏是业务特有的。比如我做过一个 IM 项目,聊天会话的缓存对象经常泄漏。
这时候就需要自定义检测规则了。
4.2.1 添加自定义监听对象
你可以通过 RefWatcher 来手动监控某个对象:
// 获取 RefWatcher 实例
RefWatcher refWatcher = LeakCanary.install(this);
// 监控一个自定义对象
ChatSession session = new ChatSession();
refWatcher.watch(session, "ChatSession");
这样当 session 被回收时,LeakCanary 会检查它是否泄漏。如果泄漏了,就会生成报告。
4.2.2 配置检测阈值
LeakCanary 默认的检测频率是每 5 秒一次。但有些场景下,这个频率太高了。比如你快速旋转屏幕,Activity 频繁重建,LeakCanary 会疯狂触发检测。
我个人习惯在开发阶段把阈值调低一点:
LeakCanary.Config config = LeakCanary.getConfig();
config.watchDurationMillis = 2000; // 2秒检测一次
config.maxStoredHeapDumps = 5; // 最多保留5个 heap dump
config.requestWriteExternalStoragePermission = false; // 不需要存储权限
如果你发现 LeakCanary 频繁弹出通知,影响开发效率,可以把
maxStoredHeapDumps 设小一点。我一般设成 3,够用就行。
4.2.3 忽略已知泄漏
有些泄漏是系统或者第三方库的,你暂时修不了。LeakCanary 允许你忽略这些泄漏:
LeakCanary.Config config = LeakCanary.getConfig();
config.ignoredRefs.add("com.tencent.smtt"); // 忽略 X5 内核的泄漏
config.ignoredRefs.add("android.os.BinderProxy"); // 忽略 Binder 代理泄漏
说白了,就是让 LeakCanary 别老拿这些已知问题来烦你。等你有空再回头处理。
4.3 源码原理剖析:它到底是怎么工作的?
用了这么久 LeakCanary,你有没有好奇过它的原理?
其实核心逻辑就三步:检测 → 抓取 → 分析。
4.3.1 检测阶段:WeakReference + ReferenceQueue
LeakCanary 的核心机制,说白了就是 Java 的 WeakReference 和 ReferenceQueue。
当一个对象被 WeakReference 包裹后,如果它被 GC 回收了,这个 WeakReference 就会被放入 ReferenceQueue。LeakCanary 就是利用这个机制来判断对象是否泄漏。
看一段简化后的源码:
public class RefWatcher {
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
public void watch(Object watchedReference, String referenceName) {
// 创建一个 WeakReference,关联到 ReferenceQueue
KeyedWeakReference weakRef = new KeyedWeakReference(
watchedReference, referenceName, queue
);
// 延迟 5 秒后检查
checkRetainedExecutor.execute(() -> {
// 如果 WeakReference 还在队列里,说明对象没有被回收
if (!weakRef.isEnqueued()) {
// 触发 GC 再试一次
gcTrigger.runGc();
// 如果还是没被回收,说明泄漏了
if (!weakRef.isEnqueued()) {
// 开始抓取 heap dump
heapDumper.dumpHeap();
}
}
});
}
}
嗯,这里有个细节:为什么触发 GC 后还要再检查一次?
因为 GC 不是立即执行的。你调用 System.gc() 只是建议,系统不一定马上响应。所以 LeakCanary 会等一小会儿再检查。
4.3.2 抓取阶段:Heap Dump
一旦确认对象泄漏,LeakCanary 就会抓取当前的内存快照(heap dump)。这个过程会暂停 App 一小会儿,但时间很短,用户基本感知不到。
抓取到的 heap dump 文件会保存在 /data/data/包名/files/leakcanary/ 目录下。文件名格式是 时间戳_dump.hprof。
4.3.3 分析阶段:寻找 GC Root 路径
这是最核心的部分。LeakCanary 会解析 heap dump 文件,找到泄漏对象到 GC Root 的引用链。
说白了,就是回答一个问题:为什么这个对象没有被回收?
LeakCanary 使用了一个叫 shark 的库来做分析。它会遍历整个对象图,找到所有强引用路径。然后按长度排序,把最短的路径展示给你。
举个例子,如果泄漏路径是:
GC Root (Main Thread)
→ ThreadLocal
→ Handler
→ Activity (泄漏)
那你就知道,是 Handler 持有了 Activity 的引用,导致它无法被回收。
LeakCanary 2.x 版本最大的变化,就是把分析过程从 App 进程移到了独立的
LeakCanary-analyzer 进程。这样即使分析过程很耗时,也不会阻塞主进程。用户体验好很多。
4.4 实战经验:如何利用 LeakCanary 快速定位问题
我在项目中遇到过很多次,LeakCanary 报告了一个泄漏,但开发同事看了半天不知道问题出在哪。其实是有技巧的。
- 先看泄漏对象类型:是 Activity、Fragment 还是其他?如果是 Activity,大概率是生命周期管理问题。
- 再看引用链:从 GC Root 到泄漏对象,中间经过哪些类?重点关注那些「不该持有长生命周期引用」的类。
- 最后看引用类型:是强引用还是软引用?强引用泄漏才是真问题。
举个例子,有一次 LeakCanary 报告 MainActivity 泄漏。引用链显示:
GC Root → HandlerThread → Handler → Callback → MainActivity
我一看就明白了:Handler 的 Callback 是一个匿名内部类,隐式持有了外部 MainActivity 的引用。而 HandlerThread 是长生命周期的,所以 MainActivity 永远无法被回收。
解决方案很简单:把 Callback 改成静态内部类,或者用 WeakReference 包裹。
我曾经遇到一个奇葩问题:LeakCanary 报告泄漏,但引用链指向的是系统类。后来发现是某个第三方 SDK 在后台持有了 Activity 的 Context。这种问题很难排查,但 LeakCanary 的引用链能帮你快速定位到具体是哪个 SDK 搞的鬼。
4.5 总结
LeakCanary 不是万能的,但它绝对是内存泄漏排查的第一道防线。
你想想看,如果没有它,你可能要等到用户反馈「App 闪退」「越用越卡」时,才后知后觉去排查。有了 LeakCanary,开发阶段就能发现问题,修复成本低得多。
我个人建议:每个 Android 项目都应该集成 LeakCanary。它不费什么功夫,但能帮你省下大量排查时间。
下一章,我会讲如何利用 LeakCanary 的报告,结合 MAT 做深度分析。敬请期待。