LeakCanary实战:从OOM崩溃到内存泄漏排查与CI集成

发布时间:2026/10/16 6:53:27
LeakCanary实战:从OOM崩溃到内存泄漏排查与CI集成 简介这是一份面向Android开发者的LeakCanary入门实践Demo聚焦内存泄漏检测这一性能优化核心场景。当Android Profiler、MAT等PC端工具不便随时使用时LeakCanary可在手机端直接捕获泄漏引用链帮助初中级开发者快速定位Activity、Fragment等对象的泄漏问题。资源包共54个文件约596KB以png截图、xml布局与配置、java源码、gradle构建脚本及properties属性文件为主另含jar、bat、pro等辅助文件完整呈现一个可运行的Android工程结构。目前已有1909人学习下载。通过这份Demo读者可以了解LeakCanary的接入方式与依赖配置观察泄漏日志与引用链输出并对照工程目录理解Application初始化、Activity生命周期与泄漏触发场景的对应关系为后续在真实项目中排查内存问题打下基础。1. 从一次 OOM 崩溃说起LeakCanary 到底在手机里装了什么线上包跑了一周后台崩溃列表里突然冒出一堆OutOfMemoryError堆栈指向一个早该销毁的 Activity。日志里 GC 次数正常内存曲线却像台阶一样只涨不跌——这就是典型的内存泄漏。很多同行第一次遇到这种问题靠adb shell dumpsys meminfo和 MAT 翻 hprof 文件一轮下来半小时起步。LeakCanary 干的事很直接把「怀疑泄漏 → 抓堆 → 找引用链」这套流程塞进 App 里自动跑在泄漏对象被 GC 回收失败时弹一条通知把最短强引用路径摆在你面前。它适合所有还在用 Java/Kotlin 写 Android、且被内存问题拖过后腿的开发者尤其是接手老项目、Activity 和 Fragment 满天飞的那种场景。这一篇不讲空泛概念只讲它怎么接、怎么读结果、哪些坑会让你误判。2. 接入与初始化从依赖到第一次泄漏通知2.1 为什么用 debugImplementation 而不是 implementationLeakCanary 的核心是ObjectWatcher加一个后台HeapAnalyzer它会持有对象引用、定时触发 GC、dump hprof 文件。这些动作在 release 包里既没必要也影响性能所以官方推荐只在 debug 变体引入。常见做法是在模块级build.gradle里写dependencies { // 只在 debug 构建中引入release 包不会打进 LeakCanary 代码 debugImplementation com.squareup.leakcanary:leakcanary-android:2.14 }这里2.14是当前较稳定的版本线2.x 之后不需要在 Application 里手动LeakCanary.install(this)它靠ContentProvider自动完成初始化。逻辑说明debugImplementation保证依赖只进 debug 变体自动安装依赖leakcanary-android里的AppWatcherInstaller它在 Application 创建前通过 ContentProvider 的onCreate完成注册。参数上唯一要留意的是版本号2.x 各小版本 API 基本兼容但 1.x 的RefWatcher写法已经废弃别照抄老博客。如果你是多模块项目且某个 library 模块也想被监控不需要在每个模块重复加依赖LeakCanary 监控的是整个进程的对象只要 app 模块引入即可。这一点常被误解成「每个模块都要加」结果依赖冲突。2.2 手动 watch 一个对象什么时候需要自己调用自动监控覆盖了 Activity、Fragment、Fragment View、ViewModel、Service 这些常见生命周期组件。但如果你怀疑的是某个自定义缓存、单例里的 Listener就得手动埋点// 在对象应该被销毁的位置调用比如自定义 View 的 onDetachedFromWindow val watcher AppWatcher.objectWatcher watcher.watch( watchedObject myCustomCache, // 被观察对象 description MyCustomCache should be destroyed // 通知里显示的描述 )逻辑说明watch会把对象包进一个KeyedWeakReference等 5 秒后触发 GC如果引用还在就判定为 retained。参数description会直接出现在泄漏通知的标题里写清楚「谁应该被销毁」否则通知列表里一堆ObjectWatcher你根本分不清。注意watch本身不阻塞主线程但频繁调用会拖慢 debug 包别在onDraw这种高频回调里埋。2.3 第一次泄漏通知长什么样装好后跑 App退出一个 Activity如果它泄漏了几秒后状态栏会出现一只金丝雀图标。点开通知你会看到三段信息Leaking 对象、GC Roots 到它的引用链、以及每个节点的类型。引用链是重点它按「最短强引用路径」排序通常第一行就是罪魁祸首。比如链里出现static field → MySingleton.instance → listener → MainActivity那基本就是单例持有了 Activity 的匿名内部类。读这条链的能力比记住任何 API 都重要。3. 读懂引用链从 retained 对象定位到那一行代码3.1 引用链的三种典型形态LeakCanary 给出的引用链不是随便排的它按引用类型和长度做了裁剪。实际排查中你会反复遇到三类第一类是静态字段持有链里出现static关键字通常是单例、工具类缓存了 Context 或 Listener。第二类是线程或 Handler链里出现ThreadLocal、MessageQueue、HandlerThread多半是匿名 Runnable 或未移除的 Message 持有了外部类。第三类是资源未关闭链里出现Cursor、InputStream、Bitmap属于注册了监听或持有 native 内存没释放。判断方法很简单看链的末端是不是你的业务类再看中间节点有没有static、Thread、Message这些关键字。定位到具体类后去搜这个类里所有赋值给静态变量、或者传给线程的地方。3.2 用 leakcanary 的 Logcat 输出做快速筛选通知适合单次排查但如果你要批量看Logcat 更高效。LeakCanary 会把分析结果打到LeakCanarytag 下# 过滤 LeakCanary 的分析日志只看 retained 对象和引用链 adb logcat -s LeakCanary:D | grep -A 20 retained逻辑说明-s LeakCanary:D只显示该 tag 的 debug 及以上日志grep -A 20把 retained 后面的引用链一起带出来。参数上D是 debug 级别如果你只想看泄漏结论可以换成E。这条命令在连续操作多个页面时特别有用能一次性收集多条泄漏记录再对照代码逐个改。3.3 一个真实误判为什么它说泄漏但其实是正常的有一种情况会让新手翻车LeakCanary 报告某个对象 retained但你检查代码发现它确实还被合法持有。常见原因是对象生命周期比预期长比如一个全局缓存故意持有 Activity 的 Context 用于预加载。这时候要看引用链里是不是有static且这个 static 是你自己设计的缓存。如果是要么改成ApplicationContext要么在description里标注这是预期行为避免团队里其他人反复排查同一个「假泄漏」。提示LeakCanary 判定的是「GC 后仍被强引用」不是「一定泄漏」。业务上合法的长生命周期持有会被误报读链时先确认持有者是不是你主动设计的。4. 避坑与排查五条血泪经验4.1 通知不弹先查这三个开关现象依赖加了跑起来却没有任何通知。原因通常有三个一是构建变体选成了 releasedebugImplementation不生效二是设备关闭了通知权限LeakCanary 的通知被系统拦了三是 App 处于后台部分 ROM 会限制后台弹通知。解决确认当前是 debug 变体去系统设置里给 App 打开通知权限排查时把 App 切到前台再退出页面。4.2 引用链指向系统类别急着改系统代码现象链的末端是android.os.Handler或android.view.inputmethod.InputMethodManager。原因这些系统类内部持有了你的 Activity但根因往往是你没在onDestroy里移除回调或解绑监听。解决顺着链往上看找到你自己的类检查removeCallbacks、unregisterReceiver、setListener(null)是否成对出现。系统类只是「最后一根稻草」不是问题本身。4.3 频繁 dump 导致 debug 包卡顿现象接入后 debug 包滑动明显掉帧甚至 ANR。原因LeakCanary 每次判定 retained 都会 dump hprof大内存 App 一次 dump 可能几百毫秒到数秒且会暂停部分线程。解决排查阶段集中操作别一边跑性能测试一边开着它如果只需要看引用链不想要完整堆可以在AppWatcher.config里调大watchDurationMillis减少触发频率。4.4 混淆后引用链变成 a.b.c现象debug 包里引用链全是a.b.c完全看不懂。原因debug 变体默认不混淆但如果你给 debug 也开了 minify类名就被改了。解决检查buildTypes.debug是否误开了minifyEnabled true排查内存问题时把它关掉。release 包本来就不该带 LeakCanary所以不存在线上混淆问题。4.5 多进程 App 只监控到主进程现象某个后台进程里的 Service 泄漏了但通知里看不到。原因LeakCanary 默认只安装在主进程其他进程的ContentProvider初始化不会触发。解决如果确实要监控多进程需要在每个进程的 Application 里手动调用安装逻辑或者用AppWatcher手动 watch。多数项目只关心主进程这条按需处理。5. 进阶把泄漏检测接进 CI 与自定义 retained 判定5.1 用 Instrumentation 测试自动断言泄漏手动点页面终究会漏成熟团队会把泄漏检测写进 UI 测试。思路是在测试结束时主动触发 GC然后检查ObjectWatcher里是否还有 retained 对象。常见做法是封装一个LeakAssertion// 在 Espresso 测试的 teardown 阶段调用 fun assertNoRetainedObjects() { // 触发一次 GC等待 LeakCanary 完成判定 Runtime.getRuntime().gc() Thread.sleep(2000) val retained AppWatcher.objectWatcher.retainedObjectCount // retainedObjectCount 大于 0 说明还有对象没被回收 if (retained 0) { throw AssertionError(检测到 $retained 个 retained 对象疑似内存泄漏) } }逻辑说明retainedObjectCount是 LeakCanary 暴露的计数表示当前仍被强引用、判定为疑似泄漏的对象数量。参数Thread.sleep(2000)是给 GC 和判定留时间实际项目里可以换成轮询直到计数稳定。这样每次 CI 跑 UI 测试泄漏会直接让构建失败比人工点页面可靠得多。5.2 自定义 retained 判定阈值默认情况下 LeakCanary 对任何 retained 对象都报警但有些场景你只关心数量超过阈值的泄漏。可以通过AppWatcher.config调整// 在 Application 中配置减少误报 AppWatcher.config AppWatcher.config.copy( watchDurationMillis 10_000, // 延长观察时间减少瞬时误判 retainedDelayMillis 5_000 // 延迟判定给 GC 更多机会 )逻辑说明watchDurationMillis控制对象被 watch 后多久开始判定retainedDelayMillis是判定前的等待时间。调大这两个值能过滤掉「刚好在 GC 边缘」的假阳性代价是泄漏发现变慢。参数没有万能值我一般先保持默认误报多了再往上加。5.3 一个我坚持了很久的习惯引用链读多了会发现90% 的泄漏就那几种静态持有、线程未停、监听未解绑、资源未关。但剩下 10% 往往藏在第三方 SDK 里链会指向某个你根本没写过的类。这时候别急着甩锅先看这个 SDK 的初始化是不是传了 Activity 的 Context。从那以后我每次接入新 SDK都会强制走一遍「传 ApplicationContext 还是 ActivityContext」的检查并在退出页面后手动 watch 一次相关对象。这个习惯帮我省掉了至少三次线上 OOM 排查。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询