2、内存泄漏检测:LeakCanary原理与集成、手动触发GC分析、hprof文件分析

内存泄漏,说白了就是本该被回收的对象,因为被某个“钉子户”一直引用着,GC拿它没办法。我见过不少项目,线上跑着跑着就OOM了,查下来全是泄漏堆出来的。今天咱们就把LeakCanary、手动GC、hprof分析这三板斧聊透。

2.1 LeakCanary原理与集成

LeakCanary这东西,我最早接触是在Square开源的时候。当时就觉得,这工具太懂Android开发者了。它的核心原理其实不复杂:

  • 监听Activity/Fragment生命周期:onDestroy之后,把对象放进一个“观察队列”
  • 等待GC:过5秒触发一次GC,看看对象还在不在
  • 如果还在:那就dump出hprof文件,分析引用链
  • 展示结果:把泄漏路径展示在通知栏或App内

嗯,这里要注意,LeakCanary 2.x版本用起来比1.x简单太多了。不需要手动初始化,加个依赖就行。

集成方式(2.x版本)

// build.gradle (app模块)
dependencies {
    // debug包才启用,release包自动忽略
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}

就这么一行。你甚至不需要写Application里初始化。LeakCanary通过ContentProvider自动注册了。我个人习惯在debug包用,release包绝对不加——毕竟性能开销还是有的。

小技巧:如果你只想在特定测试机上启用,可以用LeakCanary.Config来控制。比如只监听某些Activity,或者自定义dump目录。

// 自定义配置示例
LeakCanary.config = LeakCanary.config.copy(
    // 只保留最近7天的hprof文件
    maxStoredHeapDumps = 7,
    // 自定义监听对象
    watchDurationMillis = 5000
)

2.2 手动触发GC分析

有时候LeakCanary没报泄漏,但你觉得内存不对劲。这时候就得手动来一波GC分析。我遇到过好几次,LeakCanary没抓到,但手动一查就现原形了。

手动触发GC,说白了就是三步:

  1. 打开Profiler:Android Studio自带的Memory Profiler
  2. 点GC按钮:那个垃圾桶图标,点它
  3. 观察内存曲线:如果GC后内存没降下来,大概率有泄漏

你想想看,正常情况GC后内存应该明显下降。如果反复GC都降不下来,那肯定有对象被“焊死”在内存里了。

避坑指南:我曾经在某个项目里,发现GC后内存只降了一点点。查了半天,原来是单例里持了一个大Bitmap。单例的生命周期跟App一样长,Bitmap又没及时释放,结果就泄漏了。所以手动GC后,一定要看“哪些对象还活着”。

手动触发GC的代码方式也有:

// 代码触发GC(仅用于调试)
System.gc();
// 等GC完成
System.runFinalization();

但说实话,我很少在代码里写这个。一般都是用Profiler点按钮,更直观。

2.3 hprof文件分析

hprof文件,就是Java堆的“快照”。LeakCanary dump出来的就是它。分析hprof文件,是定位泄漏的终极手段。

我个人习惯用Android Studio自带的Profiler来打开hprof文件。操作很简单:

  • 把hprof文件拖到Android Studio里
  • 或者用File → Open → 选择hprof文件
  • Studio会自动解析,展示类、实例、引用链

分析hprof文件,核心看三个东西:

分析维度 看什么 常见问题
类列表 哪些类的实例数量异常多 Activity、Fragment、Bitmap
引用链 从GC Roots到对象的路径 静态变量、匿名内部类、Handler
大对象 占用内存最大的对象 Bitmap、byte[]、数据库游标

举个例子,你发现MainActivity的实例有5个,但按理说只应该有1个。那就点进去看引用链。引用链会告诉你:谁引用了它?为什么没被回收?

实战经验:我记得有一次,线上OOM频发。LeakCanary报了一个泄漏,但引用链指向的是系统类。我当时觉得奇怪,系统类怎么会泄漏?后来仔细看hprof,发现是WebView的setJavaScriptInterface传了一个匿名内部类,内部类隐式持有了Activity引用。嗯,这种坑最隐蔽。

分析hprof文件时,我建议你重点关注:

  • 静态集合类:HashMap、ArrayList,如果静态变量里存了Activity,必泄漏
  • 内部类:非静态内部类会持有外部类引用,特别是匿名内部类
  • Handler:MessageQueue里的Message持有Handler,Handler又持有Activity
  • 资源未关闭:Cursor、FileInputStream、BroadcastReceiver

小技巧:用Android Studio的“Analyze Stack”功能,可以直接看某个对象的GC Root路径。比手动翻引用链快多了。

最后说一句,hprof文件分析是个熟能生巧的活。刚开始你可能觉得引用链太长,看不懂。没关系,多分析几次,慢慢就找到感觉了。我刚开始也是对着hprof文件发懵,后来总结出一套方法:先看类数量,再看引用链,最后看大对象。三步走,基本能定位90%的泄漏。

好了,内存泄漏检测这块就聊到这儿。下一章咱们讲实战案例,看看真实项目里那些“坑”是怎么填的。