泄漏第一现场:使用 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
- 通过 USB 连接测试手机(≥ Android 6.0),确保开发者选项和 USB 调试已开启。
- 在 Android Studio 中点击
View → Tool Windows → Profiler。 - 选择目标进程(一般为应用包名,如
com.example.leakdemo)。
步骤 2:复现泄漏场景并抓取 Heap Dump
- 在 Profiler 中点击 Memory 标签页。
- 执行导致泄漏的复现操作(例如:进入详情页 → 返回 → 再进入,重复 3-5 次)。
- 在内存图上升到峰值时,点击 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 已干净
- 重新编译应用,再次执行相同的复现场景。
- 在 Profiler 中抓取第二个 Dump 作为 基线。
- 对比前后两次 Dump:理想情况下,Activity 实例数回归到 ≤ 1,ViewModel 实例数下降到 ≤ 1。
- 手动执行
System.gc()(在 Profiler 中点击垃圾回收按钮),观察内存是否回落到初始水平。
预防:将 Dump 分析纳入日常 CI
- 在自动化测试脚本中,在关键页面跳转前后执行
adb shell am dumpheap并保存 Dump。 - 使用 LeakCanary 或 Kubernetes(推荐)自动检测,但 Profiler 抓 Dump 依然是唯一可靠的权威验证手段。
- 每次发布前至少抓取一次全流程 Dump,对比历史基线。