Leakcanary框架分析:他是如何检测内存泄漏的?四大引用;Heap Dump的实现,设计原则

153 阅读3分钟

目录

  1. 他是如何检测内存泄漏的?监听每个四大组件的生命周期
  2. 学习他,你会知道如何设计一个好的框架,无侵入式的。

一、如何实现低侵入性?

LeakCanary 不需要手动写代码初始化,只需要在 gradle 中添加依赖就好了,然后 App 在启动时就会自动运行,年轻的时候我也非常好奇是怎么实现的,其实就是通过 ContentProvider 实现的,它可能是存在感最低的四大组建,但是Android 系统特性,应用启动时会自动初始化所有注册在 AndroidManifest.xml 中的 ContentProvider,这个初始化在 Application.onCreate() 之前执行。

image.png

所以他会在这个类里面进行初始化。

image.png

二、他是如何检测内存泄漏的?

1.1 监听Activity的生命周期

接下来,我们看看Application的registerActivityLifecycleCallbacks方法

图片.png

通过 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}")

            }
            // 其他生命周期方法空实现...
        })

Leakcanary里面进行了初始化

image.png

image.png

1.2 如何监控对象,检查是否存在泄漏

在了解是否存在泄漏,我们需要先了解一下什么是引用。

因为在Activity销毁的时候,他需要判断哪些对象可以被回收,哪些不可以。为什么不可以,就跟他引用类型有关。

  1. 强引用(Strong Reference):默认引用类型,通过 new 关键字创建。只要强引用存在,对象​​不会被垃圾回收(GC)​​。当所有强引用断开(如 obj = null),对象才会变为可回收。
Object obj = new Object(); // 强引用

3. 软引用(Strong Reference):描述有用但非必需的对象,适用于内存敏感缓存。内存充足时,对象保留;​​内存不足时,GC 可能回收​​。

SoftReference<Object> softRef = new SoftReference<>(new Object());

5. 弱引用:强度低于软引用,对象只能存活到下一次 GC。无论内存是否足够,GC 运行时必回收​​。下一次 GC 触发时回收。

WeakReference<Object> weakRef = new WeakReference<>(new Object());

7. #### 虚引用:最弱引用,无法通过 get() 获取对象,仅用于跟踪回收状态。

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);

好的,了解了这些引用知识后,我们就可以知道,如果有哪些在页面销毁后,我们触发GC,但是对象都无法回收,那么就是内存泄漏了。

但我们如何判断对象是否可以被回收呢?这里就需要引入一个新的概念,引用队列。

引用队列(ReferenceQueue)是内存泄漏检测的 ​​事件触发器​​ 和 ​​资源清理器​​,它解决了两个关键问题:

  1. ​精准判断对象是否被回收​​(避免误判/漏判)
  2. ​高效清理无效引用​​(避免内存浪费)

引用队列​​ 是一个用于跟踪对象回收状态的工具,当使用 WeakReferenceSoftReferencePhantomReference 时,若关联的引用队列(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()
        }
    }

    // 监控所有 Activity
    private 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
    }
}

图片.png

双击打开他,就会自动打开android studio的Profile

​(1) 匿名内部类泄漏​
  • ​特征​​:
    类名包含 $1$2(如 MainActivity$1)。
  • ​分析步骤​​:
    查看引用链中是否有 HandlerRunnableThread 持有外部类(如 Activity)的引用。
​(2) 单例/静态变量泄漏​
  • ​特征​​:
    类名包含 ManagerUtilsInstance,或字段被 static 修饰。
  • ​分析步骤​​:
    检查单例对象是否直接或间接持有了 Context/Activity。
​(3) 未反注册监听器​
  • ​特征​​:
    引用链中出现 BroadcastReceiverEventBusOnClickListener 等监听器。
  • ​分析步骤​​:
    检查这些监听器是否在 Activity 销毁时被反注册。

图片.png 点击Jump toSource就可以调整到问题的地方。其实原理就是解析了hprof文件,LeakCanary 的堆分析引擎 ​​Shark​​ 是开源的,可直接集成到代码中解析 .hprof


三、为什么要学习他的代码,我们要了解他的开发设计思维

  1. 如何实现无侵入式

  2. 要考虑,写一次代码,其他关联的地方都会增加,而不是每次用到相关的,我都要增加。当然这个适用于有这种全局监听的,也就是有一个上层的,或者你可以增加一个中间层去实现。

四、leakcanary不能完全解决内存泄漏问题

  1. LeakCanary仅监控特定对象​​:默认只检测 ActivityFragment 的泄漏
  2. 内存抖动(Memory Churn)​​:频繁创建/销毁对象导致 GC 压力,LeakCanary 无法直接检测。
  3. LeakCanary官方建议仅在 Debug 构建中使用,无法监控生产环境问题。

LeakCanary 用于日常开发预防,Profiler 用于深度优化和疑难杂症。

  1. Profiler定期手动检查内存使用。
  2. 在出现性能问题时深入分析。