简介:这是一份面向Android开发者的LeakCanary入门实践Demo,聚焦内存泄漏检测这一性能优化核心场景。当Android Profiler、MAT等PC端工具不便随时使用时,LeakCanary可在手机端快速定位泄漏对象与引用链,适合刚接触内存优化、需要动手验证的初中级开发者。资源包共54个文件,约596KB,包含4个Java源码文件、10个XML布局与配置、3个Gradle构建脚本,以及properties、iml、jar等工程配置与依赖文件,另有若干png截图和bin缓存文件,构成一个可直接导入Android Studio运行的完整工程。目录按标准Gradle结构组织,app模块下src、libs、build.gradle、proguard-rules.pro等一应俱全,便于对照源码理解LeakCanary的接入方式与观察流程。目前已有1909人学习下载,可作为内存泄漏排查的练手起点,帮助读者在真实工程中建立检测、分析、修复的完整思路。
1. 从一次 OOM 崩溃说起:LeakCanary 到底在手机上干了什么
线上灰度包跑了两天,崩溃后台里OutOfMemoryError的堆栈开始扎堆,机型集中在内存 4G 以下的中低端设备。翻代码翻到凌晨也没看出哪块大对象没释放,最后靠一个只在 debug 包里挂着的工具,五分钟定位到一个静态 Handler 持有了 Activity。这个工具就是 LeakCanary,一个把「内存泄漏」这件玄学事变成可复现报告的开源库。它做的事很朴素:在 Activity、Fragment 这些对象本该被回收时挂一个弱引用观察者,等 GC 走完一轮如果对象还在,就 dump 堆内存、算最短引用链,把「谁拽着你不放」直接画出来。适合所有做 Android 的从业者,尤其是被 OOM 和卡顿追着跑、又不想天天开 Android Studio Profiler 的人。它不解决泄漏,它只负责让你看见泄漏。
2. 接入与初始化:三行依赖背后的自动装配逻辑
2.1 为什么 LeakCanary 的接入成本能压到这么低
很多人第一次用 LeakCanary 会觉得「不真实」——就加一行依赖,什么初始化代码都没写,它就开始工作了。这背后是 Android 的ContentProvider自动装配机制在兜底。库在 manifest 里注册了一个LeakCanaryInstaller(不同版本类名略有差异),应用启动时系统会在Application.onCreate之前回调它的onCreate,LeakCanary 就在这个时机完成AppWatcher的安装和ObjectWatcher的启动。所以严格说不是「零初始化」,而是初始化被藏进了 ContentProvider 的生命周期里。
这个设计带来一个必须知道的边界:如果你的项目做了启动优化,把 ContentProvider 批量禁用了,或者用了某些激进的多进程方案,LeakCanary 可能压根没装上。常见做法是接入后先跑一次,看 Logcat 里有没有LeakCanary相关的启动日志,确认它真的活着,再谈后面的事。
2.2 依赖配置与版本选择
在模块级build.gradle里加依赖,注意只加在 debug 变体上,release 包不要带:
dependencies { // 只在 debug 构建里引入,避免污染线上包体积和性能 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14' }逻辑说明:用debugImplementation而不是implementation,是因为 LeakCanary 会周期性触发 GC、dump 堆快照,这些操作在线上是纯负担。参数说明:版本号写你项目能兼容的最新稳定版即可,2.x 系列 API 相对稳定,2.14 是较新的一个版本,具体以你依赖仓库里能拉到的为准。如果你的项目还在用 1.x,那套 API 和 2.x 差异很大,RefWatcher需要手动初始化,本文按 2.x 讲。
加完依赖同步一下,直接跑 debug 包。如果 Logcat 里出现LeakCanary is running之类的字样,说明装配成功。没出现就回到 2.1 检查 ContentProvider 是否被禁用。
2.3 手动监控非 Activity 对象
自动监控只覆盖 Activity、Fragment、Fragment View、ViewModel、Service 这几类。如果你怀疑某个自定义对象、某个单例里的集合泄漏了,得手动 watch:
class ImageLoader { fun load(url: String) { // 业务逻辑 } } // 在合适的时机,比如对象该被释放的地方 val loader = ImageLoader() AppWatcher.objectWatcher.watch( watchedObject = loader, description = "ImageLoader 实例,预期在页面销毁后释放" )逻辑说明:watch会给传入对象挂一个弱引用,并记录一个带description的 key。等 GC 后如果这个弱引用没被清掉,LeakCanary 就认为它泄漏了,报告里会显示你写的描述。参数说明:watchedObject是你要观察的对象,description一定要写清楚「这个对象本该在什么时机被回收」,否则报告出来你只看到一堆类名,还是不知道哪出的问题。这个描述字段是很多人忽略的后悔药,写和不写,排查效率差一倍。
3. 读懂泄漏报告:从 Logcat 到引用链的完整排查路径
3.1 通知栏与 Logcat 两条入口
LeakCanary 发现泄漏后有两个出口。一是通知栏,会弹一条「某应用泄漏了 N 个对象」的通知,点进去是详情页,能看到引用链的图形化展示。二是 Logcat,过滤LeakCanary这个 tag,能看到完整的文本报告。我一般习惯先看 Logcat,因为文本可以复制、可以搜,图形界面适合给不熟悉的人看。
报告的核心结构是「LeakTrace」,它从 GC Root 一路列到泄漏对象,中间每一跳都标了引用类型。你要找的是那条「不该存在的引用」——通常是某个静态字段、某个单例的成员、某个线程的局部变量。
3.2 引用链的读法:从 GC Root 往下找断点
一份典型的报告长这样(简化):
┬─── │ GC Root: Global variable in native code │ ├─ android.os.HandlerThread instance │ Leaking: NO (it's a GC root) │ ↓ HandlerThread.contextClassLoader ├─ dalvik.system.PathClassLoader instance │ Leaking: NO │ ↓ PathClassLoader.runtimeInternalObjects ├─ com.example.MySingleton class │ Leaking: NO │ ↓ static MySingleton.instance │ ~~~~~~~~~~~~~~~ ├─ com.example.MySingleton instance │ Leaking: UNKNOWN │ ↓ MySingleton.context │ ~~~~~~~~~~~~~~~ ╰→ com.example.MainActivity instance Leaking: YES (ObjectWatcher was watching this)读法是从下往上看:最底下╰→那个Leaking: YES的就是泄漏对象,也就是本该被回收的 Activity。往上每一层是「谁在引用它」。关键看带~~~下划线的那一行,那是 LeakCanary 帮你标出的「可疑引用点」。上面这份报告里,MySingleton.instance这个静态字段持有了context,而context就是 Activity,链条闭合,泄漏坐实。
逻辑说明:Leaking: NO表示这一跳是正常的、不该被怀疑的引用;Leaking: UNKNOWN表示 LeakCanary 无法判断;Leaking: YES就是问题对象。参数说明:报告里还会带Retained size,表示这个对象及其独占引用链占了多少内存,数值大的优先处理。
3.3 常见泄漏类型的报告特征
| 泄漏类型 | 报告特征 | 典型引用链 |
|---|---|---|
| 静态字段持有 Context | 静态变量下划线标注 | static 字段 → Activity |
| 非静态内部类 / Handler | 匿名类持有外部类 | Handler → 外部类 → Activity |
| 线程未停止 | Thread 对象存活 | Thread → Runnable → Activity |
| 监听器未注销 | 集合类持有 | Listener 集合 → 回调 → Activity |
| 资源未关闭 | Cursor / Stream 存活 | 静态缓存 → 资源对象 |
这张表不是让你背,是让你在报告里看到对应结构时能快速归类。比如看到Thread出现在引用链中间,基本就是线程没停或者线程池没关。
3.4 用 heap dump 文件做二次分析
LeakCanary 会把每次泄漏的堆快照存成.hprof文件,路径在应用私有目录下,通知详情页里能导出。如果引用链看得不够清楚,可以把 hprof 拖进 Android Studio 的 Profiler,或者用 MAT 打开,按类名搜、按 retained size 排序,能看到更完整的内存分布。这一步属于进阶,日常排查里 LeakCanary 自己的报告已经能解决八成问题。
4. 避坑与常见问题:那些让报告失真的细节
4.1 报告说泄漏,但代码看着没问题
现象:报告明确指出某个 Activity 泄漏,引用链也清晰,但你把那条链上的代码翻了三遍,逻辑上确实该释放。
原因:最常见的是 LeakCanary 的判定时机问题。它在 GC 后观察弱引用,但 GC 不是实时的,某些对象可能只是「还没轮到被回收」,被误判成泄漏。另一种情况是对象确实被释放了,但释放发生在 LeakCanary 的观察窗口之后。
解决:先看报告里的Leaking: YES是否稳定复现。如果同一个页面每次进出都报,那是真泄漏;如果偶发一次,可能是误报。可以在AppWatcher配置里调大watchDurationMillis(默认 5 秒),给 GC 更多时间。另外确认你的测试设备没有开「不保留活动」这类开发者选项,那会干扰生命周期。
4.2 接入后应用变卡、启动变慢
现象:加了 LeakCanary 之后,debug 包启动明显变慢,滑动也偶尔卡顿。
原因:LeakCanary 在后台会周期性触发 GC 和堆快照,堆快照(dump hprof)是个重操作,会暂停应用线程。对象越多、堆越大,暂停越久。
解决:这是 debug 工具的正常代价,别在性能测试时开着它。如果实在影响开发体验,可以在AppWatcher.config里关掉自动监控,只在你需要的时候手动 watch。另外把LeakCanary.config.dumpHeap设为 false 可以只保留引用链分析、不存 hprof,能省不少时间。
4.3 多进程下报告丢失或不准
现象:应用有多个进程,主进程能收到泄漏报告,某个子进程里的泄漏完全没反应。
原因:LeakCanary 默认只在它被安装的那个进程里工作。子进程如果没有独立安装,就不会监控。
解决:确认子进程是否也需要监控。如果子进程里有 Activity 或大量对象,需要在子进程的初始化里也装上 LeakCanary,或者用AppWatcher.manualInstall在子进程手动安装。注意多进程下每个进程会各自 dump,报告是分开的。
4.4 混淆后引用链变成 a.b.c
现象:release 包(如果误开了 LeakCanary)或者开了混淆的 debug 包,报告里的类名全是a.b.c,完全没法看。
原因:代码混淆把类名、字段名都改了,LeakCanary 拿到的就是混淆后的名字。
解决:LeakCanary 本身不需要额外 keep 规则,但你要看报告就得保留 mapping 文件,用retrace工具把混淆后的堆栈还原。更省事的做法是 debug 包别开混淆,或者给关键类加 keep。线上包本来就不该带 LeakCanary,所以这个问题主要出现在「想在生产环境临时排查」的场景,那种情况建议用别的方案。
4.5 报告里全是系统类,找不到自己的代码
现象:引用链中间一大段都是android.os.、java.lang.开头的系统类,翻半天看不到自己写的类。
原因:系统类的引用链本来就长,LeakCanary 会把完整链路都列出来,不会帮你过滤。
解决:从下往上找第一个你自己包名下的类,那才是问题起点。系统类那几跳通常是MessageQueue、Thread、ClassLoader这类框架层的东西,它们本身不泄漏,只是「路过」。把注意力放在你包名下的那个类上,看它为什么还活着。
5. 进阶技巧:把泄漏检测塞进 CI 和自定义规则
5.1 用 Instrumentation 测试自动跑泄漏检查
LeakCanary 提供了leakcanary-android-instrumentation这个测试依赖,可以在 Espresso 或 UI 自动化测试里断言「没有泄漏」。思路是:测试跑完某个页面后,主动触发一次 GC,然后检查ObjectWatcher里还有没有未释放的对象。
// 测试依赖:androidTestImplementation 'com.squareup.leakcanary:leakcanary-android-instrumentation:2.14' class LeakTest { @get:Rule val leakRule = LeakCanaryRule() @Test fun activityShouldNotLeak() { // 启动并销毁目标 Activity ActivityScenario.launch(MainActivity::class.java).close() // LeakCanaryRule 会在测试结束时自动检查并断言 } }逻辑说明:LeakCanaryRule是一个 JUnit Rule,它在测试方法结束后触发 GC 并检查是否有对象泄漏,有的话直接让测试失败。参数说明:这个方案适合放进 CI,每次提交跑一遍,把泄漏挡在合并之前。注意它依赖 instrumentation 环境,跑起来比普通单元测试慢,建议只对核心页面做。
5.2 自定义 ObjectWatcher 的监控范围
默认监控的那几类对象不一定覆盖你的场景。比如你有一套自研的页面容器,或者用了某种自定义的 View 复用机制,可以扩展InstallableWatcher来接入:
class MyCustomWatcher : InstallableWatcher { override fun install() { // 注册你的生命周期回调,在对象该释放时调用 watch } override fun uninstall() { // 反注册 } } // 安装 AppWatcher.manualInstall( application = this, watchersToInstall = AppWatcher.appDefaultWatchers(this) + MyCustomWatcher() )逻辑说明:manualInstall允许你完全接管监控器的安装,把默认的几类和你自己的 watcher 一起装上。参数说明:watchersToInstall是个列表,appDefaultWatchers返回默认那几类,加号后面接你自己的。这个用法适合框架层开发者,业务开发一般用不到。
5.3 一个我踩过的坑:别在 Application 里持有 LeakCanary 的引用
有次为了在代码里动态开关监控,我把AppWatcher的配置对象存成了一个静态字段,结果 LeakCanary 自己报了自己泄漏——那个静态字段持有配置,配置又间接持有 Application Context,链条绕回来了。血泪经验是:LeakCanary 的配置改完就完事,别长期持有它的任何对象。从那以后我每次改AppWatcher.config都只做一次性赋值,绝不往静态变量里塞。希望帮到你。
本文还有配套的精品资源,点击获取