目录
- 他是如何检测内存泄漏的?监听每个四大组件的生命周期
- 学习他,你会知道如何设计一个好的框架,无侵入式的。
一、如何实现低侵入性?
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()
}
}
// 监控所有 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
)。 - 分析步骤:
查看引用链中是否有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定期手动检查内存使用。
- 在出现性能问题时深入分析。