
简介在Android性能优化中内存优化必不可少而解决内存泄漏正是其核心关键。面向Android开发者LeakCanary简单使用演示工程聚焦于内存泄漏检测与PC端Android Profiler、MAT等工具相比它直接在手机端运行适合日常开发中快速定位泄漏点。压缩包内含54个文件以Java源码、Gradle构建脚本、XML配置和PNG说明图为主另有少量jar、properties及iml文件Java源码展示接入LeakCanary的核心逻辑Gradle脚本管理依赖与构建XML定义清单与布局PNG直观呈现泄漏检测效果整体仅596KB轻量易读便于快速查阅。目前已有1912人学习下载可见该资源受到不少Android开发者的关注适合初步接触内存优化的开发者参考。通过示例工程可了解LeakCanary的接入方式、初始化配置与泄漏检测结果展示配合图片说明能快速理解内存泄漏的常见触发场景同时为后续在真实项目中集成和自定义LeakCanary提供可运行模板有助于开发者在此基础上扩展出完整的泄漏监控体系。1. 让 LeakCanary 开口说话内存泄漏不是玄学是没接对工具很多人把 Android 内存泄漏当成“玄学”线上包闪退、Android Studio Profiler 里看到内存曲线一路向右上角爬却不知道泄漏点到底在哪。其实 LeakCanary 这个开源库解决的问题非常具体——它在你自己的 App 里埋了一个内存侦探Activity 或 Fragment 销毁后如果还被人引用它会在通知栏直接弹一条泄漏报告告诉你泄漏对象的引用链谁被谁持有哪个对象该回收却没回收。LeakCanary 内存泄漏检测最爽的地方在于你不需要会看 hprof 大文件打开通知点进详情一行一行引用链就能定位到可疑持有者。适合所有做 Android 的开发者尤其是维护过几十个 Activity 的中大型项目的朋友——它能把“排查泄漏”这事从一天缩短到半小时。2. 依赖配置与自动初始化十分钟让泄漏在通知栏当场现形2.1 为什么不建议只靠 ProfilerLeakCanary 的选型理由Android 内存泄漏检测不是只有 LeakCanary 一条路。你可以在 Android Studio 里用 Profiler 手动 dump 堆也可以用 adb 命令抓 hprof 再用 MAT 分析但这两条路的共同痛点是你得先“猜到”某个页面可能有泄漏然后卡着时机去 dump运气不好的时候错过现场泄漏已经回收了啥都看不出来。LeakCanary 的做法是持续守护。它在你的进程里维护一个 RefWatcher当 Activity 或 Fragment 执行 onDestroy 之后它会拿一个弱引用去观察这些本该被回收的对象如果过了一段时间弱引用没有被清理它就手动触发一次 GC再确认一次仍然活着就判定为泄漏然后立即做一次堆转储把引用链拉出来给你看。这一套“怀疑—确认—转储—出报告”的流程不需要人工介入所以它特别适合放进日常开发回归里。版本上LeakCanary 从 2.x 开始做了大量简化核心依赖是 leakcanary-android它利用 AndroidX 的 App Startup 机制自动完成初始化你在 Application 类里基本不用写一行 install 代码。这个改动很关键——以前 1.x 时代大家经常忘掉在自定义 Application 里调用 LeakCanary.install()忘了就静默失效2.x 之后这种低级坑少了一大半。要注意的是它必须在 debug 包上使用不要在 release 包启用后面避坑章节会详细讲。2.2 在 build.gradle 里接入依赖并验证自动初始化Gradle 依赖是接入的第一步我通常会把版本号单独提出来维护方便后续升级然后在 debugImplementation 里声明。这里的核心逻辑是debug 包自动开启检测release 包完全不引入依赖这样发布包体积和性能都不受影响。android { defaultConfig { // 用变量统一管理版本升级时只改一处 buildConfigField String, LEAK_CANARY_VERSION, \$leakcanaryVersion\ } } dependencies { // debug 包专用release 包不会打包进去 debugImplementation com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion // 可选在测试包里也开启检测 testImplementation com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion }上面这段配置里有几个参数值得说。debugImplementation是关键它保证了 LeakCanary 只参与 debug 构建不会进入 release 产物leakcanaryVersion是一个占位变量你可以改成当前可用的版本号新版不再需要单独的 leakcanary-support-fragment 依赖Fragment 监控已经内置。首次接入后建议把 App 跑起来然后反复进入退出几个页面再观察 logcat 里有没有 LeakCanary 的启动日志。我一般会看到类似LeakCanary is running and ready to watch的输出看到这行就说明自动初始化成功。2.3 手动初始化与 RefWatcher盯住你自定义的长生命周期对象自动初始化只覆盖 Activity 和 Fragment但业务里最常见的泄漏源反而是自定义对象比如一个单例持有某个 View、一个全局的 EventBus 订阅器没注销、一个网络的回调持有 Activity 的 Context。LeakCanary 提供 RefWatcher 让你手动注册任意对象。在 1.x 时代你需要自己写一个自定义 Application 来 new 一个 RefWatcher2.x 之后官方 API 变得更简洁我更推荐用 LeakCanary 自带入口来 watch 对象。public class DemoApplication extends Application { Override public void onCreate() { super.onCreate(); // 1.x 时代的手动初始化方式2.x 不再需要这行 // 但如果你要从自定义代码里 watch 目标对象可以这样 if (LeakCanary.isInAnalyzerProcess()) { return; } // 注册任意需要监控的对象这里模拟一个被单例持久的 View View targetView new View(this); ObjectWatcher watcher LeakCanary.getObjectWatcher(); watcher.watch(targetView, main_activity_root_view); } }逻辑说明LeakCanary.isInAnalyzerProcess()判断当前进程是不是 LeakCanary 的分析进程如果是就直接 return避免重复初始化ObjectWatcher.watch(targetView, description)的第二个参数是描述信息会在泄漏报告里原样展示方便你一眼知道这个对象是哪来的。参数上要注意不要在 Activity 的 onCreate 里 watch 它自己Activity 的监控是自动的你手动 watch 反而会在每个页面都打一条无意义的报告。RefWatcher 的核心思路是只监控“你心里清楚它应该死但怕它没死”的对象。3. 从通知点进堆转储读 hprof 与分析引用链的实战姿势3.1 默认流程泄漏发生到通知展示要多久LeakCanary 内部的判定流程比很多人想象的严格它不会对象一销毁就立刻报泄漏。默认情况下它先拿弱引用观察对象然后等待 5 秒让 GC 有机会回收如果仍未回收再主动触发一次 GC再次确认后才做堆转储。整个从“对象被销毁”到“通知栏弹出报告”通常需要 10 秒左右这个延迟是故意的——为了减少误报给 JVM 充分的回收机会。一次典型的触发流程如下开发者打开某个页面退出LeakCanary 在后台完成弱引用检查判定泄漏后它会把 hprof 文件写入应用私有目录然后在一个独立进程里做分析分析完成通知栏弹出一条报告内容包含泄漏对象、泄漏总量、参考链。这里要注意分析工作是在独立进程完成的所以分析过程中即使 App 卡顿也不会堵住 UI 线程。3.2 读参考链三个关键字段看懂每次泄漏点进报告最核心的是引用链那一长串很多人第一次看会懵。其实整条链就三部分最上面是LeakSingle泄漏对象本身中间是一串“谁持有谁”的节点最后指向GC Root垃圾回收根。引用类型里最常见的是 static、局部变量、监听器回调我用一张表说明常见引用类型引用类型常见场景是否该背锅static 字段单例、静态集合、Application 持有多数是泄漏源头局部变量未取消的 Handler、Runnable看是否依附 Activity匿名内部类回调持有外部类引用非常经典监听器/观察者EventBus、LiveData 观察者未移除常被忽略例如一个典型报告里你看到MainActivity被MainActivity$1持有而MainActivity$1又被Handler持有那就说明匿名 Handler 持有 Activity 超过了生命周期。读到这种链不需要继续往下翻修改方案基本已经确定了把匿名内部类改成静态内部类加弱引用或者在 onDestroy 里 removeCallbacks。3.3 用 adb 手动导出 hprof给 MAT 做二次验证LeakCanary 自动分析的引用链已经足够定位 90% 的泄漏但有时候你要看更多细节比如某个对象的具体内存大小、多个泄漏之间的关联这时候手动 dump 一份 hprof 文件拿去分析是常识。LeakCanary 本身也会自动生成 hprof 文件路径通常在/sdcard/Download/leakcanary/或者 App 私有目录你可以直接用 adb 拉出来。# 先确认包名再查找 LeakCanary 生成的 hprof 文件 adb shell run-as com.example.demo find / -name *.hprof 2/dev/null # 如果文件在私有目录用 run-as 拉取到本地 adb exec-out run-as com.example.demo cat /data/data/com.example.demo/files/leakcanary/demo.hprof ./demo.hprof # 如果文件在共享存储直接 pull 即可 adb pull /sdcard/Download/leakcanary/xxx.hprof ./这里的run-as只适用于 debug 包release 包没有调试权限这也再次说明 LeakCanary 只适合 debug 使用。拉到本地后用 Memory Analyzer 打开 hprof打开Dominator Tree输入泄漏对象的类名过滤能看到这个对象的保留堆大小——这个数字能告诉你泄漏严重程度。LeakCanary 自己给的引用链已经定位了“谁引用它”MAT 则帮你回答“它占了多大内存”两者互补。4. LeakCanary 使用中的避坑与常见问题排查改了四版依赖依旧爆红那晚4.1 现象依赖加了以后编译直接失败提示找不到 androidx 类原因LeakCanary 2.x 强制要求项目已经迁移到 AndroidX如果你还在用旧的支持库或者 applicationId 里没有启用 jetifier它会在编译阶段爆出一堆找不到类的错误。解决方式分两种项目已经 AndroidX 化直接升级编译版本项目还在用 support 库那就只能退回 LeakCanary 1.6.x 版本。我踩过一次当时整个团队以为依赖写法错了改了四版 Gradle 配置都没用最后发现是项目整体还没迁移 AndroidX。这个坑在旧项目接 LeakCanary 时几乎必踩。4.2 现象手动 watch 后无论有没有泄漏通知栏永远不弹报告原因漏写了LeakCanary.isInAnalyzerProcess()的进程判断。如果你在 Application 的 onCreate 里做了初始化而 LeakCanary 的分析进程也会执行到这段代码它会在分析进程里也初始化一遍导致对象 watcher 冲突报告永远发不出来。解决方式很简单在 Application 开头加一行if (LeakCanary.isInAnalyzerProcess()) return;这是官方推荐的写法绝大多数“不弹通知”的问题都出在这。4.3 现象报告里出现大量误报弱引用对象频繁被判泄漏原因LeakCanary 的判定机制是“弱引用仍未回收则触发 GC 再确认”但有些对象本身生命周期很长例如 Application 持有的全局配置对象、懒加载的单例它们在页面销毁后依然被合理持有。解决方式这类对象不要用 ObjectWatcher 去 watch它不属于内存泄漏。我给新人的建议是第一条规则只 watch 你确信“应该死”的对象比如某个只应该存活在页面里的自定义 View全局单例永远不要喂给 RefWatcher。4.4 现象release 包崩溃堆栈指向 LeakCanary 相关类原因把 leakcanary-android 写成了 implementation而不是 debugImplementation导致它被打进发布包。LeakCanary 官方声明过不要混淆它所以 release 包要么没配混淆规则要么跑起来就崩。解决方式最稳妥的是依赖加上debugImplementation限定同时保留官方推荐的混淆规则如果你确实要在 release 里临时开那就得配合混淆 keep。还有一种常见情况是 release 包禁用了minifyEnabledLeakCanary 的逻辑跑起来没问题但某些版本与 R8 配合不完整——所以归根结底还是别让 LeakCanary 出现在 release 里。4.5 现象多进程 App 每次启动都弹泄漏报告原因LeakCanary 在 monitoring 进程和 analyzer 进程之间做通信如果你的 App 本身是多进程架构LeakCanary 默认只关心主进程但如果你在子进程里也创建了 Application它的 Activity 生命周期监听也会触发导致子进程里销毁的 Activity 也被监控一看全是噪音报告。解决方式Application 里判断如果LeakCanary.getObjectWatcher()已初始化且当前不是主进程直接不执行后续代码。5. 把 LeakCanary 从人工点按接到 CI让每次回归自动跑一轮内存泄漏冒烟LeakCanary 的常规用法是开发时点开通知栏看报告但如果只靠人工触发很容易漏检。我习惯的做法是用 adb 写一个简单的压力脚本在打包机上模拟用户反复进出高危页面然后等 LeakCanary 自动产出报告再通过 shell 检查通知栏日志。这套流程不需要额外装任何工具核心就是一个循环脚本。# 模拟用户高频进出 5 个高危页面 for i in 1 2 3 4 5; do adb shell am start -n com.example.demo/.MainActivity sleep 2 adb shell input keyevent KEYCODE_BACK sleep 2 done # 等待 LeakCanary 分析完成从日志里抓取报告关键词 adb logcat -d | grep -i leakcanary | grep -i leak这段脚本里am start负责拉起页面input keyevent模拟返回键退出页面循环里反复进出把疑似泄漏对象“钓”出来。grep -i leak会抓到两类内容一是 LeakCanary 自己的运行日志二是泄漏分析报告的关键行。如果有泄漏logcat 里会出现Leak found字样。我一般会把这个脚本挂在 GitLab CI 的 instrumentation 任务后面每次合并前跑一遍十几秒就能确认这次改动有没有引入新的泄漏点。另外还有一个日常习惯值得养成每次提交代码前把改动涉及的页面手动进出 3 次然后看一眼通知栏有没有新的泄漏通知。启动、切后台、返回、退出这四个动作覆盖了绝大多数 Fragment 和 Activity 泄漏的触发路径。这个习惯花不了两分钟却能在代码 review 之前拦下七成无意识泄漏。我自己的教训是刚接入 LeakCanary 时把它当成了“有问题再打开的工具”后来发现这样效果最差因为泄漏不像崩溃会立刻出现它要积累很多次页面切换才显形。从那以后我强制自己把 LeakCanary 检测跑进每一条 feature 分支的验证流程配合 adb 脚本做高频进出每次合并前完整过一遍“进出高危页面 检查通知栏日志”的组合拳再提 MR。用了两个迭代之后新引入的内存泄漏基本在一周内都能被逮到这个习惯一直保持到现在。希望帮到你。本文还有配套的精品资源点击获取