当前位置: 首页 > news >正文

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

目录

  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()}}// 监控所有 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)。
  • ​分析步骤​​:
    查看引用链中是否有 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. 在出现性能问题时深入分析。

相关文章:

  • 深度学习在语音识别中的应用
  • 性能比拼: Deno vs. Node.js vs. Bun (2025版)
  • LLM做逻辑推理题 - 如何找出不标准的球?
  • Rabbitmq集群重启操作
  • 交易系统的构建与实战法则
  • 《解锁图像“高清密码”:超分辨率重建之路》
  • leetcode 674. Longest Continuous Increasing Subsequence
  • LLM MCP模型上下文协议快速入门(for Java)
  • B端管理系统:企业运营的智慧大脑,精准指挥
  • FPGA——DDS信号发生器设计
  • Qt UDP 通信的详细实现步骤和示例代码
  • 系统思考:危机中的转型机遇
  • JVM虚拟机--JVM的组成
  • PyTorch深度学习框架60天进阶学习计划 - 第46天:自动化模型设计(一)
  • 虚拟现实(VR)技术在教育领域的创新应用
  • 某局部三层休闲娱乐中心建筑设计与结构设计
  • 【GlobalMapper精品教程】094:GlobalMapper26简体中文版安装教程(附安装包下载)
  • Docker Overlay 网络的核心工作(以跨节点容器通信为例)
  • Java集合及面试题学习
  • C# 变量||C# 常量
  • 我国成功发射试验二十七号卫星01星~06星
  • 人民网评:官方轻踩刹车,智能驾驶不能“蒙眼狂奔”
  • 一场小型越野赛为何吸引众多越野大神打卡?
  • 2025年上海版权宣传周在杨浦启动
  • 业绩激活新消费,押中爆款哪吒IP的泛娱乐龙头卡游再冲港股IPO
  • 美联储主席警告:特朗普关税政策“极有可能”推高物价