Leakcanary框架分析:他是如何检测内存泄漏的?四大引用;Heap Dump的实现,设计原则
目录
- 他是如何检测内存泄漏的?监听每个四大组件的生命周期
- 学习他,你会知道如何设计一个好的框架,无侵入式的。
一、如何实现低侵入性?
LeakCanary
不需要手动写代码初始化,只需要在 gradle
中添加依赖就好了,然后 App
在启动时就会自动运行,年轻的时候我也非常好奇是怎么实现的,其实就是通过 ContentProvider
实现的,它可能是存在感最低的四大组建,但是 ContentProvider
他有一个特点,在 App
初始化的过程中也会初始化 ContentProvider
二、他是如何检测内存泄漏的?
1.1 监听Activity的生命周期
接下来,我们看看Application的registerActivityLifecycleCallbacks方法
通过 registerActivityLifecycleCallbacks()
方法,Application
可以监听所有 Activity 的生命周期回调。
LeakCanary 在初始化时,通过 Application
注册 ActivityLifecycleCallbacks
,从而自动监控所有 Activity 的 onDestroy()
事件。
这也是他的高明之处,无侵入式。LeakCanary 只需在 Application
中注册一次,即可覆盖所有 Activity,无需在每个 Activity 中手动添加代码。
简略代码如下
// 注册 Activity 生命周期回调application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {override fun onActivityDestroyed(activity: Activity) {// 当 Activity 销毁时,将其交给 watchObject 监控watchObject(activity, "Activity: ${activity.javaClass.simpleName}")}// 其他生命周期方法空实现...})
1.2 如何监控对象,检查是否存在泄漏
在了解是否存在泄漏,我们需要先了解一下什么是引用。
因为在Activity销毁的时候,他需要判断哪些对象可以被回收,哪些不可以。为什么不可以,就跟他引用类型有关。
- 强引用(Strong Reference):默认引用类型,通过
new
关键字创建。只要强引用存在,对象不会被垃圾回收(GC)。当所有强引用断开(如obj = null
),对象才会变为可回收。
Object obj = new Object(); // 强引用
- 软引用(Strong Reference):描述有用但非必需的对象,适用于内存敏感缓存。内存充足时,对象保留;内存不足时,GC 可能回收。
SoftReference<Object> softRef = new SoftReference<>(new Object());
- 弱引用:强度低于软引用,对象只能存活到下一次 GC。无论内存是否足够,GC 运行时必回收。下一次 GC 触发时回收。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
好的,了解了这些引用知识后,我们就可以知道,如果有哪些在页面销毁后,我们触发GC,但是对象都无法回收,那么就是内存泄漏了。
但我们如何判断对象是否可以被回收呢?这里就需要引入一个新的概念,引用队列。
引用队列(ReferenceQueue
)是内存泄漏检测的 事件触发器 和 资源清理器,它解决了两个关键问题:
- 精准判断对象是否被回收(避免误判/漏判)
- 高效清理无效引用(避免内存浪费)
引用队列 是一个用于跟踪对象回收状态的工具,当使用 WeakReference
、SoftReference
或 PhantomReference
时,若关联的引用队列(ReferenceQueue
),对象被垃圾回收后,对应的引用(Reference)会被自动加入队列。
接下来,我们知道了哪些对象会被回收,那么我们只需要进行对比,不能被回收的activity实例,就存在内存泄漏。
// 极简版内存泄漏检测工具(仅监控 Activity)
class MiniLeakCanary private constructor(private val context: Context) {// 引用队列,用于判断对象是否被回收private val referenceQueue = ReferenceQueue<Any>()// 存储被监控对象的弱引用private val watchedObjects = mutableMapOf<String, WeakReference<Any>>()companion object {fun install(application: Application) {MiniLeakCanary(application).watchActivities()}}// 监控所有 Activityprivate fun watchActivities() {(context as MyApp).registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {override fun onActivityDestroyed(activity: Activity) {Log.e("MiniLeakCanary", "onActivityDestroyed")watchObject(activity, "Activity: ${activity.javaClass.simpleName}")}})}// 监控任意对象fun watchObject(obj: Any, tag: String) {val ref = WeakReference(obj, referenceQueue)//为什么叫引用队列呢?里面存储了多少数据?watchedObjects[tag] = ref//只有一个数据,为什么要用map来存储。因为他是监听所有的activity的。checkLeak()}// 检查泄漏private fun checkLeak() {Log.d("checkLeak", "checkLeak: "+watchedObjects.size)// 移除已被回收的引用var ref: Reference<out Any>?while (referenceQueue.poll().also { ref = it } != null) {watchedObjects.entries.removeAll { it.value == ref }}// 延迟 5 秒后再次检查(模拟 LeakCanary 等待 GC)Handler(Looper.getMainLooper()).postDelayed({triggerGcAndCheck()}, 5000)}// 触发 GC 并检查未回收对象private fun triggerGcAndCheck() {// 触发 GC(仅调试用,生产环境不推荐)Runtime.getRuntime().gc()System.runFinalization()// 检查未被回收的对象watchedObjects.forEach { (tag, ref) ->if (ref.get() != null) {Log.e("MiniLeakCanary", "可能内存泄漏: $tag")// 此处可生成 Heap Dump(需复杂实现)}}Log.d("MiniLeakCanary", "triggerGcAndCheck: ")}
}
1.3 Heap Dump的实现
上述,我们只是知道了那个Activity泄漏了,但是不知道具体是那个对象。我们看到Leakcanary里面都有的。所以下面我们来看看Heap Dump的实现。
-
Heap Dump
- 通过
Debug.dumpH()
生成.hprof
文件。 - 文件路径通常存放在应用缓存目录。
- 通过
-
泄漏分析
- 解析
.hprof
文件,找到未回收的KeyedWeakReference
。 - 通过引用链分析,定位泄漏路径。
- 解析
代码实现:我们搞一个有问题的代码
class MainActivity : AppCompatActivity() {private val TAG = "MainActivity"private val REQUEST_CODE_LOCATION = 1// 静态变量持有 Activity 实例(导致泄漏的关键)companion object {var leakedActivity: MainActivity? = null}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)insets}this.findViewById<TextView>(R.id.tv_hello).setOnClickListener {var intent = Intent(this, Main2Activity::class.java)intent.putExtra("name", "Lance");intent.putExtra("boy", "23");startActivity(intent)finish()}// 将当前 Activity 赋值给静态变量leakedActivity = this}
}
增加dumpHeap的实现。
// 触发 GC 并检查未回收对象
private fun triggerGcAndCheck() {// 触发 GC(仅调试用,生产环境不推荐)Runtime.getRuntime().gc()System.runFinalization()// 检查未被回收的对象watchedObjects.forEach { (tag, ref) ->if (ref.get() != null) {Log.e("MiniLeakCanary", "可能内存泄漏: $tag")// 此处可生成 Heap Dump(需复杂实现)val heapDumpFile: File = dumpHeap()!!Log.e("LeakDetector","内存泄漏 detected! Heap dump saved to: $heapDumpFile")}}Log.d("MiniLeakCanary", "triggerGcAndCheck: ")
}// 生成 Heap Dump 文件
private fun dumpHeap(): File? {val heapDumpDir: File = File(myapp.getExternalFilesDir(null), "heap_dumps")if (!heapDumpDir.exists()) {heapDumpDir.mkdirs()}val fileName = "leak_dump_" + System.currentTimeMillis() + ".hprof"val heapDumpFile = File(heapDumpDir, fileName)try {Debug.dumpHprofData(heapDumpFile.absolutePath)return heapDumpFile} catch (e: IOException) {Log.e("LeakDetector", "生成 Heap Dump 失败", e)return null}
}
双击打开他,就会自动打开android studio的Profile
(1) 匿名内部类泄漏
- 特征:
类名包含$1
、$2
(如MainActivity$1
)。 - 分析步骤:
查看引用链中是否有Handler
、Runnable
或Thread
持有外部类(如 Activity)的引用。
(2) 单例/静态变量泄漏
- 特征:
类名包含Manager
、Utils
、Instance
,或字段被static
修饰。 - 分析步骤:
检查单例对象是否直接或间接持有了 Context/Activity。
(3) 未反注册监听器
- 特征:
引用链中出现BroadcastReceiver
、EventBus
、OnClickListener
等监听器。 - 分析步骤:
检查这些监听器是否在 Activity 销毁时被反注册。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
点击Jump toSource就可以调整到问题的地方。其实原理就是解析了hprof文件,LeakCanary 的堆分析引擎 Shark 是开源的,可直接集成到代码中解析 .hprof
。
三、为什么要学习他的代码,我们要了解他的开发设计思维
-
如何实现无侵入式
-
要考虑,写一次代码,其他关联的地方都会增加,而不是每次用到相关的,我都要增加。当然这个适用于有这种全局监听的,也就是有一个上层的,或者你可以增加一个中间层去实现。
四、leakcanary不能完全解决内存泄漏问题
- LeakCanary仅监控特定对象:默认只检测
Activity
和Fragment
的泄漏 - 内存抖动(Memory Churn):频繁创建/销毁对象导致 GC 压力,LeakCanary 无法直接检测。
- LeakCanary官方建议仅在 Debug 构建中使用,无法监控生产环境问题。
LeakCanary 用于日常开发预防,Profiler 用于深度优化和疑难杂症。
- Profiler定期手动检查内存使用。
- 在出现性能问题时深入分析。