1. LeakCanary原理入门:弱引用与 ReferenceQueue 如何定位泄漏
说实话,我刚开始接触 Android 内存泄漏时也是一头雾水。总觉得泄漏是个玄学问题,直到我把 LeakCanary 的源码翻了几遍,才恍然大悟——它背后的原理其实非常朴素。今天我就带你从最核心的两个概念讲起:弱引用(WeakReference) 和 引用队列(ReferenceQueue)。
1.1 为什么 LeakCanary 能自动发现泄漏?
你想想看,一个对象如果在生命周期结束后还一直被引用,GC 就没办法回收它。这就是泄漏。但问题是,我们怎么知道某个对象是否应该被回收?
LeakCanary 的思路很简单:它先假设一个对象是安全的,然后过一段时间再去检查。如果对象还在,那就说明它被某处持有了。嗯,这里的关键是——检查的方式不能影响对象本身的生命周期。如果用强引用去检查,那对象永远不能被回收,测出来全是「泄漏」。所以它用了弱引用。
核心逻辑:弱引用不阻止 GC 回收。当弱引用指向的对象只被弱引用可达时,GC 会立刻回收它。LeakCanary 就利用这个特性来「观察」目标的存活状态。
1.2 弱引用 + ReferenceQueue 的配合机制
弱引用 本质上是一个轻量级包装。你创建一个弱引用指向 Activity 或 Fragment,然后把这个弱引用注册到一个 ReferenceQueue 里。当 GC 回收了目标对象,弱引用就会被自动放入队列中。
我用一个比喻帮你理解:
- 弱引用就像你雇了一个侦探,他偷偷跟着目标(Activity)
- ReferenceQueue 就像侦探的传唤器
- 当目标被「消灭」(GC 回收),侦探就会按一下传唤器,报告说「目标已消失」
- 如果你等了一段时间还没收到报告,那就说明目标还活着——这就是泄漏
为什么必须用 ReferenceQueue 而不是主动轮询?我在项目中踩过这个坑。如果你手动去判断一个弱引用的 get() 是否为 null,可能会遇到对象明明被回收了但引用还没被清除的情况(因为弱引用的清除时机和 GC 并发有关)。而 ReferenceQueue 是线程安全的、由 JVM 保证的机制,可靠得多。
1.3 手写一个迷你版 LeakCanary 核心逻辑
下面这段代码,你完全可以自己跑跑看。它展示了 LeakCanary 最本质的检测思路。
class MiniLeakDetector {
// 保存弱引用和其对应的 ReferenceQueue
private val refQueue = ReferenceQueue<Any>()
private val watchedReferences = mutableListOf<WeakReference<Any>>()
fun watch(target: Any) {
val weakRef = WeakReference(target, refQueue)
watchedReferences.add(weakRef)
}
fun detectLeak(): List<Any> {
// 先触发一次GC,清除已被回收的对象
System.gc()
Thread.sleep(100)
// 从队列中取出所有已被回收的弱引用
var ref = refQueue.poll()
while (ref != null) {
watchedReferences.remove(ref as WeakReference<*>)
ref = refQueue.poll()
}
// 剩下的就是还没被回收的——可能泄漏
return watchedReferences.mapNotNull { it.get() }
}
}
这段代码虽然粗糙,但已经能工作。我在某次内部工具开发中直接用类似逻辑找出了几个 ViewPager 里的 Fragment 泄漏。当然 LeakCanary 正式版要比这健壮得多,它做了内存 dump、hprof 解析、调用链分析等。
1.4 流程图:弱引用 + ReferenceQueue 检测泄漏的全过程
下边这张图我手绘了核心流程,你重点关注「GC 回收后弱引用入队」和「队列中无该引用 = 泄漏」这两个关键判断点。
1.5 为什么选择弱引用而不是软引用?
我之前看有些同学问,为什么不用 SoftReference?其实软引用的回收时机比弱引用晚——只有在即将 OOM 时才会回收。如果你用软引用去检测泄漏,可能对象早就该被回收了,但因为内存还够,软引用一直不肯清掉,导致误报。说白了,软引用太「恋旧」了。而弱引用会在 下一次 GC 时立即清理,是最敏感的观察者。
| 引用类型 | 回收时机 | 用在 LeakCanary 中? |
|---|---|---|
| 强引用 | 从不主动回收 | ❌ |
| 软引用 | 内存不足时回收 | ❌ 太迟 |
| 弱引用 | GC 时立即回收 | ✅ 最佳选择 |
| 虚引用 | GC 后入队,无法获取对象实例 | ❌ 无法判断对象是否还活着 |
1.6 你可能会遇到的坑
我曾经 在低版本 Android 设备上发现,手动调用 System.gc() 并不总能立即触发回收。LeakCanary 的标准做法是:先调用 gc(),然后 System.runFinalization(),再等一小段时间(约 100ms)。如果这样还不奏效,它会尝试多次 GC+等待 的循环。你写自己的检测工具时一定要注意这个细节。
个人经验:如果你在调试时发现 LeakCanary 在某个特定机型上漏报或误报,先检查这个机型的 GC 实现是否标准。比如某些华为、小米定制系统对弱引用的处理有差异,我曾遇到过弱引用被长时间「挂起」的问题。
1.7 本章小结
LeakCanary 的核心原理,说白了就是三步:
- 埋点:用弱引用包装目标对象,注册到 ReferenceQueue
- 等待:触发 GC,给 JVM 机会回收对象
- 验证:检查队列中是否有对应的弱引用——有则安全,无则泄漏