第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 方案:

  1. Android Studio Profiler 自动转储:在 Memory Profiler 面板点击 "Dump Java Heap",文件自动保存到 captures/ 目录(.hprof 格式)。
  2. adb 命令行手动触发:使用 am dumpheap 命令,适合自动化脚本或无 IDE 环境。
  3. 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 本身也可能失败,导致排查中断。故障树如下:

Heap Dump 导出故障树 Dump 导出失败 / 文件不可用 权限不足 设备内存不够 非 debuggable 路径不可写 堆冻结 ANR Signature 未签名 分区存储限制 堆太大 > 1GB 改用 run-as 或 profileable 导出至 download 目录 使用 KOOM 轻量快照

修复动作与复测:确保 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 采集,避免依赖手动操作:

  1. 代码级触发:在自定义 Application 的 onTrimMemory() 中判断堆增长趋势,自动触发 Debug.dumpHprofData()。
  2. 回传通道:使用 WorkManager 在 WIFI+充电状态下将 Dump 文件上传至内部文件服务器,命名规则:app_pkg_deviceId_timestamp.hprof。
  3. 白名单策略:只在 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,可大幅缩小泄漏怀疑范围。