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

发布于:2025-04-20 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

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

一、如何实现低侵入性?

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销毁的时候,他需要判断哪些对象可以被回收,哪些不可以。为什么不可以,就跟他引用类型有关。

  1. 强引用(Strong Reference):默认引用类型,通过 new 关键字创建。只要强引用存在,对象​​不会被垃圾回收(GC)​​。当所有强引用断开(如 obj = null),对象才会变为可回收。
Object obj = new Object(); // 强引用
  1. 软引用(Strong Reference):描述有用但非必需的对象,适用于内存敏感缓存。内存充足时,对象保留;​​内存不足时,GC 可能回收​​。
SoftReference<Object> softRef = new SoftReference<>(new Object());
  1. 弱引用:强度低于软引用,对象只能存活到下一次 GC。无论内存是否足够,GC 运行时必回收​​。下一次 GC 触发时回收。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
  1. 虚引用:最弱引用,无法通过 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
    }
}

在这里插入图片描述

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
点击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. 在出现性能问题时深入分析。

网站公告

今日签到

点亮在社区的每一天
去签到