泄漏第一现场:使用 Android Profiler 抓取 Heap Dump

现象识别:什么时候该抓 Heap Dump?

ViewModel 内存泄漏的外在信号往往在界面交互的后期才显现。如下图所示,当你在课程案例 App 中反复进入 / 退出目标详情页,并观察到以下任一现象时,应立即进入数据采集阶段:

  • GC 频繁触发:Logcat 中出现大量 I/art: Background partial concurrent mark sweep GC freed ...(Android 8 以下)或 I/Perf: GC_Reason_Alloc。
  • 页面切换卡顿:返回上一页耗时超过 500ms,且 onDestroy() 未在 1s 内被调用。
  • OOM 崩溃:堆内存稳定攀升后直接触发 OutOfMemoryError。

将这些现象与故障树根因节点对照,快速判断是否需要抓 dump:

故障树节点判断规则抓 Dump 的必要性
Activity 实例数 > 1多次进入 / 退出后 adb shell dumpsys activity 显示同一 Activity 有多个实例必须抓
ViewModel 未释放在 Activity 销毁后,ViewModelStore 中仍有该 ViewModel 引用必须抓
Fragment 重建泄漏Fragment 的 onDestroy() 后,其 ViewModel 仍存活推荐抓
内存抖动Android Profiler 内存图出现“锯齿状”频繁升降可选,先打点分析
最佳实践: 在复现场景前,先在 Profiler 中点击“Record”录制一段时间,当观察到内存明显增大时立刻抓 Dump,避免错过泄漏现场。

数据采集:三步抓取 Heap Dump

步骤 1:连接设备并打开 Profiler

  1. 通过 USB 连接测试手机(≥ Android 6.0),确保开发者选项和 USB 调试已开启。
  2. 在 Android Studio 中点击 View → Tool Windows → Profiler。
  3. 选择目标进程(一般为应用包名,如 com.example.leakdemo)。

步骤 2:复现泄漏场景并抓取 Heap Dump

  1. 在 Profiler 中点击 Memory 标签页。
  2. 执行导致泄漏的复现操作(例如:进入详情页 → 返回 → 再进入,重复 3-5 次)。
  3. 在内存图上升到峰值时,点击 Dump Java Heap 按钮(垃圾桶图标右侧)。

也可通过 ADB 命令直接抓取:

# 获取进程 PID
adb shell ps | grep com.example.leakdemo
# 假设 PID 为 12345
adb shell am dumpheap 12345 /data/local/tmp/dump.hprof
# 拉取到本地
adb pull /data/local/tmp/dump.hprof ~/Desktop/heap_dumps/

步骤 3:加载 HPROF 文件并转换为 Android 格式

从设备拉取的原始 .hprof 是 Dalvik 格式,需要转换为 Android Studio 可识别的标准 HPROF:

# 方法一:直接在 Profiler 中点击“Load from file…”选择原始文件
# 方法二:使用命令行工具
hprof-conv -z /path/to/dump.hprof /path/to/convert.hprof

转换后,Android Studio 会自动解析对象树、引用链和内存占用。

原因定位:利用 Profiler 的对象统计与引用分析

打开加载后的 Dump 文件,Profiler 会展示以下核心视图:

  • Class List(按类名排序)—— 快速找到 ViewModel、Activity、Fragment 类。
  • Reference Tree(引用树)—— 从指定对象向上追溯 GC Roots 的路径。
  • Instance View —— 查看每个实例的字段值和内存地址。

针对 ViewModel 泄漏,使用以下判断规则定位根因:

观察指标正常值泄漏迹象
目标 Activity 实例数≤ 1≥ 2(且都有 ViewModel 引用)
目标 ViewModel 实例数≤ 1≥ 2(由不同 Activity 持有)
ViewModel.mCleared 标志true(已清理)false(未清理)
GC Root 路径长度≤ 3(通常是 static 或 ActivityThread)出现匿名内部类或回调引用(路径 > 5)
特别注意: 当 ViewModel 中持有 Activity、Context、Fragment、View、BroadcastReceiver 等长生命周期对象时,即使调用了 onCleared(),如果外部注册的回调未注销,ViewModel 仍会通过回调引用泄漏 Activity。

修复动作:从 Dump 结果直接定位到代码

在 Profiler 中点击可疑的 ViewModel 实例,选择 Use Selected → Find Leaking Reference(若有插件),系统会自动高亮导致泄漏的引用链。常见的修复模式:

  • 将 startActivityForResult 改为 ActivityResultLauncher,避免 Activity 隐式引用。
  • 在 ViewModel.onCleared() 中取消注册 LiveData 观察者或清除 RxJava 订阅。
  • 使用 Application context 而非 Activity context 注入 ViewModel。

复测:验证 Heap Dump 已干净

  1. 重新编译应用,再次执行相同的复现场景。
  2. 在 Profiler 中抓取第二个 Dump 作为 基线。
  3. 对比前后两次 Dump:理想情况下,Activity 实例数回归到 ≤ 1,ViewModel 实例数下降到 ≤ 1。
  4. 手动执行 System.gc()(在 Profiler 中点击垃圾回收按钮),观察内存是否回落到初始水平。

预防:将 Dump 分析纳入日常 CI

  • 在自动化测试脚本中,在关键页面跳转前后执行 adb shell am dumpheap 并保存 Dump。
  • 使用 LeakCanary 或 Kubernetes(推荐)自动检测,但 Profiler 抓 Dump 依然是唯一可靠的权威验证手段。
  • 每次发布前至少抓取一次全流程 Dump,对比历史基线。
Heap Dump 分析流程图(泄漏现场) 现象识别 GC频繁 / 卡顿 / OOM 抓取 Heap Dump Profiler 或 adb 加载 Dump hprof-conv 转换 对象统计 Activity / ViewModel 实例数 引用链分析 追溯 GC Root 路径 定位泄漏根因 匿名内部类 / 回调 / context 关键判断规则 Activity 实例数 ≥ 2 → 泄漏 ViewModel.mCleared==false → 未清理