内存泄漏检测工具:Android Profiler 与 MAT 使用归纳
内存泄漏,说白了就是本该被回收的对象,因为被某个地方强引用了,导致 GC 死活清理不掉。我做性能优化这几年,一大半 ANR 排查现场,最后都指向了内存泄漏。泄漏了还问为什么卡?你想想看,堆里塞满了僵尸对象,GC 频繁触发,主线程被活活拖死,ANR 就来了。
今天聊聊我平时用得最顺手的两个工具:Android Profiler 和 MAT(Memory Analyzer Tool)。一个负责现场抓取,一个负责深度解剖,配合好了,大部分泄漏问题都能揪出来。
1. Android Profiler 怎么抓泄漏现场
Android Studio 自带的 Profiler 已经非常强了。我个人习惯是长按 CPU 和内存的 Record 按钮,在操作可疑场景前点一下。但注意,Profiler 的 Heap Dump 才是我们查泄漏的主要手段。
操作步骤很简单:
- 运行 App 到目标页面,反复进入退出几次(模拟用户操作)
- 在 Memory 面板点击 Dump Java Heap
- Dump 完成后,选择 App Heap 视图,切到 Package Tree View
- 按包名展开,看到自己应用的对象数量和大小
但说实话,Profiler 自带的 Analysis Tasks 只能识别一些明显的泄漏模式(比如 Activity 泄漏、Fragment 泄漏),对于一些复杂的引用链分析,它不够用。我之前遇到过 RecyclerView 的嵌套 ViewHolder 泄漏,Profiler 根本没看出来。
2. MAT 才是深度解剖的手术刀
MAT 是 Eclipse 基金会的老项目,但我敢说至今没有哪个内存分析工具能完全替代它。它的核心三板斧:Histogram、Dominator Tree、GC Root Path。
先用 Profiler 导出 hprof,再转换格式:
# 用 Android SDK 下的 hprof-conv 工具
hprof-conv -z dump.hprof dump_standard.hprof
打开 MAT,加载转换后的文件。我是怎么操作的?上来第一步先点 Histogram:
- 按对象数量排序,看哪些类实例数量异常大
- 右键 -> Merge Shortest Paths to GC Roots -> exclude all phantom/weak/soft references
- 这时就能看到泄漏对象的引用链,谁一直抱着它不放
第二步切到 Dominator Tree。这个视图按“支配树”的方式展示内存占用,你可以瞬间找到哪个对象“领导”了一大片内存。我记得有一次查视频缓存泄漏,就是靠 Dominator Tree 发现一个 Bitmap 池里的引用没有清理。
第三步,也是最硬核的——GC Root Path。从泄漏对象出发,找到它到 GC Root 的完整路径。当一个对象被 static 变量、Thread 的局部变量 或 系统类加载器 引用时,它就变成了不可回收的。我曾经追过一个 ANR,原因是 WebView 的 JavaScript 接口强引用了一个 Activity,导致 Activity 实例泄漏,GC 无法回收,最终触发 20 秒无响应。就是靠 GC Root Path 定位的。
核心流程总结:
- Profiler Dump 堆快照
- 转换 hprof 格式
- MAT 打开,看 Histogram → 找异常实例数
- 看 Dominator Tree → 确认内存大头
- 看 GC Root Path → 定位泄漏根源
3. 对比一下两个工具的优劣势
为了让你快速决策什么时候用哪个,我整理了一个表格:
| 维度 | Android Profiler | MAT |
|---|---|---|
| 上手难度 | 低,IDE 内置可视化 | 中高,需要理解堆概念 |
| 实时性 | 可以边操作边录制 | 只能做离线分析 |
| 引用链追踪 | 只显示简单路径 | 完整 GC Root 路径 |
| 大堆性能 | 卡顿明显(尤其 500MB+) | 较稳定,但加载慢 |
| 常见泄漏识别 | 内置 Activity/Fragment 检测 | 全手动,但灵活性高 |
| 我最常用场景 | 初步验证是否存在泄漏 | 深挖泄漏根因 |
hprof-conv -z 这个命令一定要跑。
4. 实战中的小技巧
说了这么多工具用法,再分享几个我积累的经验:
- 别在 Release 包上抓堆快照。Release 包混淆了,类和成员变量名都变成了 a、b、c,MAT 里根本看不出谁是谁。Debug 包或 debuggable=false?不行的,至少要保持 minifyEnabled=false。
- 多抓几次对比。我习惯在进入页面后抓一次 Heap Dump,退出后再抓一次。如果退出后某些对象仍然存活,那就高度怀疑泄漏了。
- 重点关注 android.app.Activity 实例数。Activity 是最容易泄漏的。打开 Histogram,查找
Activity,如果数量等于你打开的页面数,那就正常;如果远大于,恭喜你,中奖了。 - 用 MAT 的 OQL 做快速查询。比如查询所有 Activity 对象:
SELECT * FROM INSTANCEOF android.app.Activity。这比手动点来点去快多了。
提示:如果堆快照太大(>1GB),MAT 可能因为内存不足崩溃。建议增大 MAT 的 Xmx 参数:MemoryAnalyzer.exe -vmargs -Xmx4g。我在 8GB 内存的 Windows 上设 4GB 一般够用。
5. 组合拳:从 ANR 日志反推内存泄漏
有些时候 ANR 还没发生,但我们从 traces 文件里看到主线程正在执行 Handler.dispatchMessage,或者 ViewRootImpl 的相关操作,紧接着 GC 大量触发。这时候别急着分析 ANR 本身,先去抓一下堆快照看看是不是内存泄漏了。
我处理过一个典型案例:一个列表页每 5 秒刷新数据,Fragment 被销毁后没有移除 Handler 的回调,导致 Fragment 的 View 一直被 Handler 持有。Profiler 看到 Fragment 实例数持续增长,MAT 的 GC Root Path 指向了 Handler.mQueue 里的一个 Message 对象。修复很简单:在 onDestroy 里 handler.removeCallbacksAndMessages(null)。
嗯,今天的内容差不多就这些。工具只是辅助,关键是脑子里要有“引用链”的意识。下次你遇到 ANR,不妨先问自己:是不是又有个对象被静态变量或后台线程偷偷抱住了?