4、内存泄漏检测工具:LeakCanary原理与使用、MAT分析、Android Profiler实战
内存泄漏,说白了就是本该被回收的对象,因为被某个地方一直引用着,GC拿它没办法。时间一长,堆内存越吃越多,最后就是OOM。我见过不少项目,线上崩溃有一半都是这个原因。今天咱们就把三把利器——LeakCanary、MAT、Android Profiler——挨个捋一遍。
4.1 LeakCanary:自动化的泄漏侦探
LeakCanary是我个人最常用的工具。它不需要你手动dump堆,也不需要你懂太多底层原理。集成之后,一旦发生泄漏,通知栏直接弹出来,告诉你“哥们,这里漏了”。
4.1.1 原理:它怎么知道漏了?
核心思路其实很简单:监听Activity或Fragment的onDestroy。当它们销毁后,LeakCanary会等几秒,然后手动触发一次GC。如果对象还在,那就说明被某个引用链拽住了。
具体流程是这样的:
- 通过Application.registerActivityLifecycleCallbacks监听Activity生命周期
- 在onDestroy后,将Activity对象放入一个弱引用(WeakReference)
- 等待5秒,触发GC,再检查弱引用是否被回收
- 如果没回收,就dump堆快照,分析引用链
嗯,这里要注意:它用的是弱引用+引用队列的组合。弱引用指向Activity,如果Activity本该被回收,弱引用就会进入引用队列。如果队列里没出现,那就说明还有强引用在。
核心代码片段(简化版):
// 伪代码,展示核心逻辑
class RefWatcher {
private val queue = ReferenceQueue<Any>()
fun watch(watchedObject: Any) {
val weakRef = KeyedWeakReference(watchedObject, queue, key)
// 延迟5秒后检查
checkRetained(weakRef)
}
private fun checkRetained(ref: KeyedWeakReference) {
// 手动触发GC
Runtime.getRuntime().gc()
System.runFinalization()
// 检查引用队列
if (ref.get() != null) {
// 泄漏了!dump堆
dumpHeap()
}
}
}
4.1.2 使用:三步搞定
集成LeakCanary非常简单。我习惯在debug模式下开启,release下关闭。
// build.gradle
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.12'
}
// Application中无需额外代码,自动初始化
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// 2.x版本自动注册,不需要手动写
}
}
跑起来之后,如果有泄漏,通知栏会弹出。点进去就能看到完整的引用链。我曾经在一个老项目里,一打开就发现十几个泄漏,全是静态变量持有Activity导致的。
避坑指南:我曾经遇到过LeakCanary误报的情况。比如某些三方SDK在后台持有Activity引用,但其实是设计如此。这时候可以加@IgnoreLeak注解跳过,或者配置白名单。
4.2 MAT:堆快照的显微镜
LeakCanary能告诉你“漏了”,但有时候你想自己深挖。比如看看某个对象到底占了多少内存,或者对比两个堆快照的差异。这时候MAT就派上用场了。
4.2.1 获取堆快照
你可以用Android Studio的Profiler直接dump,也可以用adb命令:
// 先找到进程ID
adb shell ps | grep com.your.package
// dump堆到文件
adb shell am dumpheap com.your.package /data/local/tmp/heap.hprof
// 拉取到本地
adb pull /data/local/tmp/heap.hprof .
注意:Android导出的hprof格式和标准Java不一样,需要用Android Studio的sdk工具转换一下:
// 转换命令
hprof-conv heap.hprof heap-converted.hprof
4.2.2 常用分析技巧
打开MAT后,我一般先看这几个视图:
- Histogram:列出所有类的实例数量和占用大小。按Retained Heap排序,最大的往往就是问题所在。
- Dominator Tree:展示对象之间的支配关系。说白了就是谁“控制”着谁,谁占了大头。
- Path to GC Roots:从泄漏对象出发,找到GC Roots的引用链。这是定位泄漏的杀手锏。
举个例子:你发现Bitmap对象特别多,右键选择“Merge Shortest Paths to GC Roots”,排除软引用、弱引用,剩下的就是强引用链。我经常在这里发现某个静态集合里存了一堆图片没清理。
注意:MAT分析时,如果堆文件太大(超过1GB),建议加大MAT的启动内存。在MemoryAnalyzer.ini里修改-Xmx参数,比如-Xmx2048m。
4.3 Android Profiler:实时监控的仪表盘
LeakCanary是事后诸葛亮,MAT是事后解剖。而Android Profiler可以让你在运行时实时观察内存变化。我个人习惯先开Profiler跑一遍场景,看到内存只增不减,再上LeakCanary定位。
4.3.1 基本操作
在Android Studio中,点击底部的“Profiler”标签,选择你的设备和进程。然后切换到“Memory”面板。你会看到一条实时曲线,代表Java堆、Native堆、代码、栈等各部分的内存占用。
我常用的几个操作:
- 点击“Dump Java Heap”:手动抓取当前堆快照,可以查看所有对象。
- 点击“Record Allocation”:记录一段时间内的对象分配情况。适合排查“谁在疯狂new对象”。
- 点击“Force GC”:手动触发GC,看看内存能不能降下来。
4.3.2 实战:定位频繁GC
有一次我遇到一个页面滑动特别卡。打开Profiler一看,内存曲线像锯齿一样,每隔一两秒就掉一次。这说明GC太频繁了。
我点了“Record Allocation”,然后滑动页面几秒钟。停止记录后,按包名过滤,发现有个工具类每秒都在new一个StringBuilder。其实完全可以用复用对象来解决。改完之后,曲线平滑多了。
实战小技巧:在Profiler中,你可以按住Shift键框选一段内存曲线,然后点击“Dump”只导出这一段时间的堆快照。这样文件小,分析起来也快。
4.4 三把利器的配合使用
说了这么多,它们到底怎么配合?我一般按这个流程来:
- 先用Android Profiler跑一遍,看看整体内存走势。如果发现内存只增不减,或者GC频繁,说明有问题。
- 集成LeakCanary,让它自动检测泄漏。通知栏弹出后,直接看引用链,大部分问题都能在这里解决。
- 遇到复杂情况,比如Native泄漏或者大对象问题,用MAT深挖。对比两个时间点的堆快照,找出差异对象。
举个例子:有一次线上OOM,Profiler看到Native堆涨得厉害。LeakCanary查不到,因为它是Java层的。我用MAT对比了正常和异常时的堆快照,发现有个线程池创建了大量线程没销毁。嗯,最后定位到是某个三方库的bug。
个人建议:不要等到线上出问题才想起来用这些工具。每次开发新功能,都跑一遍Profiler看看内存变化。养成习惯后,很多泄漏在开发阶段就被扼杀了。
好了,内存泄漏检测这块就聊到这儿。下一章咱们聊聊更底层的——如何从源码层面优化内存分配策略。你想想看,如果能控制对象在哪个区域分配,那性能提升可不是一星半点。