第3章 内存泄漏排查工具链:Profiler、LeakCanary、MAT、命令行工具选型与比较

3.1 现象识别:什么时候需要工具链介入?

内存泄漏不会直接崩溃,而是通过可观测的“症状”暴露出来。当出现以下异常时,应当立即启动工具链排查:

  • OOM 崩溃:堆内存耗尽,Logcat 输出 OutOfMemoryError。
  • GC 频繁:Logcat 中出现大量 GC_FOR_ALLOC / GC_CONCURRENT,UI 线程卡顿。
  • 内存只升不降:反复进出同一个 Activity/Fragment 后,内存占用量持续攀升,从不回落。
  • 基线偏离:App 空闲态内存占用远高于发版时的基线值。
快速判断规则:
  1. 如果 App 启动后总内存只升不降 → 优先用 Profiler 录制堆转储。
  2. 如果已知某个 Activity 泄漏,但不知道持有者 → 集成 LeakCanary 自动分析。
  3. 如果怀疑全局单例或线程持有 → 使用 MAT 离线解析 Dominator Tree。
  4. 如果需要集成到 CI 自动化流水线 → 使用 命令行工具 组合脚本。

3.2 数据采集与工具矩阵

不同工具覆盖“数据采集”与“原因定位”两个阶段。下面对比四个核心工具的能力与适用场景:

工具 适合阶段 核心能力 限制与注意
Android Profiler 开发期 / 实时观察 实时曲线、录制堆转储、内存分配追踪 无法直接给出 GC Root 路径;性能开销大
LeakCanary 开发期 / 集成测试 自动堆转储、自动输出泄漏引用链 有一定运行性能开销;不适合线上全量开启
MAT 线下深度分析 Dominator Tree、GC Root 路径、大对象 需离线 hprof 文件;大文件(>2G)易 OOM
命令行工具 CI / 脚本 / 线上 轻量快照、dumpsys meminfo 分类统计 无法直接定位引用链;数据需人工解读
避坑警告:
使用 MAT 分析线上 hprof 时,如果文件超过 2GB,分析端极易触发 OOM。解决方案:使用 -Xmx6g 参数启动 MAT,或先用命令行 mator 进行头部降采样。

3.3 工具选型决策树(判断规则)

面对不同场景时,按下图流向选择最有效的排查工具,避免在错误工具上浪费时间:

工具选型决策树 Android 内存泄漏工具选型决策树 从现象出发,按复现程度与深度需求选择工具 检测到内存异常 容易复现? Profiler / dumpsys 实时追踪 LeakCanary / MAT 深度分析 需要全局 Dominator Tree? MAT 离线解析 Dominator Tree LeakCanary 自动泄漏链路 修复代码 → 复测验证 是 (复现) 否 (难复现) 是 (需要全局视图) 否 (自动分析即可)
工具链本质是递进关系:
Profiler 发现异常 → LeakCanary 给出怀疑对象 → MAT 确认 GC Root 链路 → 修复代码 → 复测验证。四个工具不是互斥的,而是流水线接力。

3.4 典型修复动作与复测

基于工具输出,常见修复模式如下:

  • 静态内部类泄漏:将内部类改为静态内部类 + WeakReference。
  • 集合泄漏:确认在 onDestroy / onStop 中及时清理 Listener / Callback。
  • 单例泄漏:检查单例是否持有了 Activity Context,改为 Application Context。
  • Thread 泄漏:确保 Thread / Handler 在页面销毁时 removeCallbacks 或 interrupt。

复测标准:

  • Profiler:反复进出目标页面 10 次,内存曲线保持水平不再上升。
  • LeakCanary:连续 3 次操作未触发生成 Heap Dump。
  • MAT:对比修复前后的 hprof,确认泄漏对象实例数降为 0。
排查原则:
不要相信单次 Dump 的结果。工具给出的引用链只是“候选泄漏”,务必结合业务代码逻辑二次确认,避免误杀。

3.5 预防:将工具嵌入流程

  • 开发阶段:所有 Debug 包默认集成 LeakCanary,泄漏即弹窗。
  • CI 流水线:在自动化测试最后一步,调用 adb shell dumpsys meminfo 对比基线,超过阈值则构建警告。
  • 线上监控:通过自定义方舟或侧滑框架,采集内存快照上传统计到看板。
  • 定期审计:每两周用 MAT 对 Release 包的 hprof 做一次全局 Dominator Tree 扫描,防止存量泄漏累积。
# CI 脚本示例:内存基线检查
BASELINE=150  # 假定基线 150 MB
CURRENT=$(adb shell dumpsys meminfo com.example.app | grep "TOTAL" | awk '{print $2}')
if [ "$CURRENT" -gt "$BASELINE" ]; then
  echo "Warning: Memory exceeds baseline (${CURRENT} > ${BASELINE} MB)"
  exit 1
else
  echo "OK: Memory = ${CURRENT} MB"
fi