4、解读 Heap Dump:拿到 hprof 文件,用 MAT 或自带工具分析引用链

好不容易抓到了 hprof 文件,接下来怎么办?别急着开分析工具就一通乱点。我见过不少同事,拿到文件就怼进 MAT,然后盯着“Dominator Tree”发呆——那玩意儿容易把人带偏。其实啊,解读Heap Dump有一套稳扎稳打的套路,摸熟了以后,你也能像我一样在10分钟内定位到泄漏点。

先选工具:MAT 还是 Android Studio Profiler?

我个人习惯优先用 MAT (Memory Analyzer Tool),原因很简单:它给出的引用链更直观,而且能批量分析。Android Studio 自带的 Heap Viewer 也不错,但处理大文件时容易卡死。下面这张表格是我自己总结的选型建议:

工具适用场景我的经验
MAT大文件(>200MB)、需要精确引用链内存5GB+,导出HTML报告很方便
Android Studio Profiler中小文件、快速看一眼适合熟悉的项目,但引用链有时不完整
leakcanary 自带分析快速确认已知泄漏模式点一下就出结果,但深度分析不够
小技巧:如果hprof文件太大,先跑一遍 leakcanary 的自动分析,它会在Logcat里给出疑似泄漏的类名。拿那个类名去MAT里搜,效率翻倍。

MAT 分析四步法

拿到 hprof 文件后,我一般按下面四个步骤走。你跟着做一遍,下次自己操作时心里就有底了。

  1. 打开文件,等 Histogram 加载完 —— 别急,先看看整体对象分布。如果某个类的实例数特别多,可能就是嫌疑对象。
  2. 搜索可疑类 —— 比如 BLE 相关的 BluetoothGatt、BluetoothGattCallback、你的自定义 BleManager。右键 -> Merge Shortest Paths to GC Roots -> 选 exclude weak/soft references。
  3. 看引用链 —— MAT 会列出一条从 GC Root 到该对象的路径。重点关注那些 非预期的强引用,比如被 static 变量、Application、Activity 持有了。
  4. 对比快照 —— 如果之前做过一次正常操作的截图,用它做差值分析(Compare Basket),多出来的对象就是泄漏的。

核心要点: 引用链上的每一级都要问一句“这个对象为什么还活着?” 尤其是 BLE 回调里常见的匿名内部类,很容易隐式持有外部的 Activity。

典型案例:一个 BLE 连接的泄漏路径

我去年在一个智能穿戴项目里就遇到过典型的 BLE 泄漏。那会儿产品经理天天催,我熬了两晚上才揪出来。先看代码:

// 错误写法:匿名内部类隐式持有 Activity
class BleScanActivity : AppCompatActivity() {
    private fun startScan() {
        bluetoothLeScanner.startScan(object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult?) {
                // 这里持有外部 Activity 的引用
                updateUi(result)
            }
        })
    }
}

用 MAT 看引用链的时候,发现 BleScanActivity 的实例一直存在,顺着路径往上走:

  • BleScanActivity → LeakCanary$InternalLeakCanary(被观察者持有)
  • 但其实真正的根在 BluetoothLeScanner 内部维护的 Handler 和回调列表里。扫描没停止,那个匿名 ScanCallback 就不会释放,导致整个 Activity 泄漏。

我当时的经验是:不要只看第一层引用,要多点开几层。很多泄漏是跨线程的,比如 BLE 的回调发到主线程 Handler,而那个 Handler 又持有 Activity 的 View 引用。

SVG:Heap Dump 分析流程总图

下面这张图是我自己总结的解读流程。从打开 hprof 到定位泄漏,每一步都标出来了,你可以保存下来当 debug 时参考。

Heap Dump 解读流程(BTL泄漏场景) 打开 hprof 文件 Histogram 查可疑类 Merge Shortest Paths 分析引用链,找出非预期强引用 对比快照(Compare Basket) 定位泄漏点并修复 ⚠️ 注意:优先搜索 BluetoothGatt、自定义 BLE 类 以及 Activity/Fragment 💡 技巧:使用 “exclude weak/soft references” 只看强引用路径

我的避坑指南

说几个我踩过的坑,你遇到了可以直接跳过:

  • 别只看 Dominator Tree —— Dominator Tree 算的是“如果我干掉这个对象,能释放多少内存”。但 BLE 泄漏往往对象不大(一个 Gatt 实例也就几 KB),Dominator 排不出来。要切换到 class list 按实例数排序。
  • 小心 WeakReference 的干扰 —— 有些框架(比如 LeakCanary 自己的观察者)会用 WeakReference 指向 Activity,那不代表泄漏。一定要用 Merge Shortest Paths to GC Roots (exclude weak) 这个菜单。
  • 我曾经被系统类的缓存迷惑过 —— 有一次看到 InputMethodManager 持有一个 Activity 引用,以为是系统 Bug。后来才发现是第三方键盘库泄露的。如果你看到系统类的引用,先查一下它是不是正常缓存。
警告: BLE 的 hprof 文件往往会包含大量的系统 Bluetooth 堆栈对象(比如 BluetoothDevice、BluetoothGatt 的 shared 实例)。不要被数量迷惑:系统蓝牙服务自己也会缓存一些对象,需要区分。判断方法:
- 如果是 java.lang.ref.Finalizer 持有 → 可能是 pending 的 finalizer,不算泄漏。
- 如果是你的应用代码中的强引用持有 → 那才是泄漏目标。

如何快速定位 BLE 特有的泄漏模式

根据我的经验,大部分 BLE 泄漏都逃不出下面这三种模式。做成表格方便你对照:

模式表现引用链特征修复方法
未停止扫描Activity 退出后仍被 ScanCallback 持有Activity → ScanCallback → BluetoothLeScanner → Handler → Looper在 onDestroy 中调用 stopScan
Gatt 未关闭连接断开后 BluetoothGatt 对象残留Activity → GattCallback → BluetoothGatt → BluetoothAdapter在 onDisconnected 中调用 close()
第三方库回调未解注册库内部持有了 Activity 的 ContextActivity → 第三方库单例 → SharedPreferences / WakeLock 等使用 Application Context 或主动解绑

举个例子。有一次我排查“未停止扫描”的问题,MAT 显示引用链是这样的:

BleScanActivity instance
  └─ mScanCallback (匿名类 $0)
      └─ BluetoothLeScanner.mHandler
          └─ Handler.mLooper
              └─ (主线程 looper,GC Root)

看到了吧?链条很短,但问题就是 mHandler 里还保持着回调的引用。解决方式就是在 onPause 或 onDestroy 中调用 bluetoothLeScanner.stopScan(callback)。

善用 LeakCanary 自带的“泄漏痕迹”

其实 LeakCanary 2.x 已经帮我们做了很多工作。你点开通知栏里的泄漏报告,里面就包含了引用链的文本输出:

====================================
HEAP ANALYSIS RESULT
====================================
1 LEAK:
┬
├─ com.example.BleScanActivity
│    Leaking: YES (Activity finished)
│    GC Root: System class
│    └─ static BluetoothLeScanner.sHandler
│         └─ Handler.mQueue
│              └─ MessageQueue.mMessages
│                   └─ Message.callback
│                        └─ BleScanActivity$onScanResult$1 (anonymous)
╰─────────────────────────────

这个文本可以复制出来,直接发给同事看。不过它有时候会省略一些中间节点,所以我还是会再用 MAT 验证一次。我建议你把这个文本和 MAT 截图一起贴到 Jira 里,项目经理看了也放心。

我的个人习惯: 每次定位到一个泄漏,我就把它的引用链用文本记录下来,然后整理成“常见泄漏模式清单”。下次再遇到类似的,直接ctrl+f一搜就找到了。比如上面的例子,我就存成了#BLE01模式。

好了,解读 Heap Dump 的方法大体就是这样。不要被密密麻麻的对象吓到,一步一步来,从可疑类到引用链,再到修复验证。你手里有 LeakCanary + MAT 这两把刀,就算遇到再隐蔽的 BLE 泄漏也能拿下。