LeakCanary 实战:从 OOM 崩溃到内存泄漏精准定位与 CI 集成
简介这是一份面向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之前回调它的onCreateLeakCanary 就在这个时机完成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 这几类。如果你怀疑某个自定义对象、某个单例里的集合泄漏了得手动 watchclass 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 (its 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都只做一次性赋值绝不往静态变量里塞。希望帮到你。本文还有配套的精品资源点击获取