第4章 Heap Dump 获取与导出:触发 Dump 时机、文件导出路径、多设备兼容、低内存设备替代方案
现象识别:何时需要触发 Heap Dump?
在排查 DataStore 实例泄漏时,Heap Dump 是定位泄漏根对象(GCRoot)的核心物证。但 Dump 本身会冻结 App 数秒并占用大量磁盘空间,必须按以下判断规则选择触发时机:
| 症状 | Dump 必要性 | 触发时机 |
|---|---|---|
| Profiler 中 DataStore 实例数持续增长 | 必须 | 立即触发 |
| GC 后 DataStore 实例数不降 | 必须 | GC 完成后 2 秒内 |
| App 已出现 OOM 或接近 OOM | 必须 | 触发 Dump 并接受 App 可能 ANR |
| 仅怀疑泄漏,实例数不变 | 可选 | 先跑 48 小时再决策 |
故障树判断逻辑:若实例数增长 + GC 不回收 → 泄漏已发生 → 必须 Dump;若未增长且无增长趋势 → 先观察不急于 Dump。
数据采集:多种 Dump 获取方法
根据设备内存状态和开发环境,选择最稳定的 Dump 方案:
- Android Studio Profiler 自动转储:在 Memory Profiler 面板点击 "Dump Java Heap",文件自动保存到
captures/目录(.hprof 格式)。 - adb 命令行手动触发:使用
am dumpheap命令,适合自动化脚本或无 IDE 环境。 - Profiler 手动快照:在 Android Studio 的 "Advanced Profiling" 中启用 "Record Java Heap",适合需要时间线关联的场景。
# 标准 adb 命令:生成 /data/local/tmp/ 下的 .hprof 文件
adb root
adb shell am dumpheap com.your.app /data/local/tmp/dump.hprof
# 如果是用户debug包(无需root)
adb shell am dumpheap com.your.app /data/local/tmp/dump.hprof
# 若权限不足,可先进入shell并用 run-as
adb shell
run-as com.your.app
am dumpheap com.your.app /storage/emulated/0/Download/dump.hprof
文件导出路径:多设备兼容策略
不同 Android 版本和厂商对路径访问权限限制不一,使用以下路径对应表:
| 设备状态 | Dump 保存路径 | 导出命令 |
|---|---|---|
| 已 root 或模拟器 | /data/local/tmp/dump.hprof |
adb pull /data/local/tmp/dump.hprof . |
| debuggable 包(无 root) | 应用私有目录 /data/data/com.your.app/files/ |
adb exec-out run-as com.your.app cat files/dump.hprof > dump.hprof |
| 用户debug包(Android 10+) | /storage/emulated/0/Download/ 或共享存储 |
adb pull /sdcard/Download/dump.hprof . |
| Profileable 包(Android 10+) | /data/misc/profman/ 需要 adb shell 内 cp 到可读目录 |
adb shell cp /data/misc/profman/dump.hprof /sdcard/Download/ |
注意:Android 11+ 对 /sdcard 路径有分区存储限制,优先使用
getExternalFilesDir 或 getCacheDir 在代码内写入 Dump 文件,再通过 adb pull 导出。低内存设备替代方案:避免 Dump 期间 OOM
当设备可用内存 < 256MB 或堆内存 > 剩余内存的 70% 时,直接 Dump 极易导致 App 被系统杀死。使用以下逐步降级方案:
- 方案一:使用
android:debuggable="true"+android:largeHeap="true"增加堆上限后再 Dump。 - 方案二:改用
hprof-conv工具在设备外转换格式,减少设备端内存压力。 - 方案三:启用 Matrix-IOCanary 或 KOOM 等内存监控库的轻量级快照功能,只输出关键对象引用链而不生成完整堆转储。
- 方案四:在
onLowMemory()回调中禁止自动 Dump,改为记录对象计数并输出日志。
替代方案选择规则:内存 > 512MB → 使用 adb dumpheap;内存 256~512MB → 启用 largeHeap 后 dump;内存 < 256MB → 使用 KOOM 轻量快照 + 日志回传。
原因定位:Dump 导出的常见失败与根因
Dump 本身也可能失败,导致排查中断。故障树如下:
修复动作与复测:确保 Dump 文件可分析
成功导出后,必须验证 Dump 文件完整性再进入 Mat/LeakCanary 分析:
- 文件大小:正常堆 Dump 文件应在 10MB ~ 200MB 之间;过小可能未包含完整堆,过大可能是重复 Dump。
- 文件头检查:标准 .hprof 文件前 4 字节为
0x4A 0x41 0x56 0x41(即 "JAVA")。 - 复测动作:在导出 Dump 后立即执行
adb shell dumpsys meminfo com.your.app,对比前后 PSS 变化确认 Dump 过程中 App 未异常销毁。
# 校验文件头示例(Linux / Mac)
xxd -l 4 dump.hprof
# 输出应为 00000000: 4a41 5641 ...JAVA
# 若在 Windows,可用 PowerShell
Format-Hex -Path dump.hprof -Count 4
预防:固化 Dump 采集流程
在 CI/CD 或预发环境中集成自动化 Dump 采集,避免依赖手动操作:
- 代码级触发:在自定义 Application 的
onTrimMemory()中判断堆增长趋势,自动触发Debug.dumpHprofData()。 - 回传通道:使用
WorkManager在 WIFI+充电状态下将 Dump 文件上传至内部文件服务器,命名规则:app_pkg_deviceId_timestamp.hprof。 - 白名单策略:只在 debuggable 包或指定测试设备上开启自动 Dump,避免影响线上用户。
// Kotlin 示例:自动触发 Dump 并保存至外部缓存
fun triggerHeapDump(context: Context, tag: String) {
val file = File(context.cacheDir, "dump_${tag}_${System.currentTimeMillis()}.hprof")
Debug.dumpHprofData(file.absolutePath)
// 后续通过 WorkManager 上传
}
最佳实践:在每次 DataStore 实例引用关系变更后(如 Activity.onDestroy / ViewModel.onCleared),立即执行一次条件性 Dump,可大幅缩小泄漏怀疑范围。