虚引用
在了解 LeakCanary 之前,先来了解下虚引用。
虚引用必须与 ReferenceQueue 一起使用,当 GC 准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的 ReferenceQueue 中。也就是说 ReferenceQueue 中的对象,就是被成功回收对象的虚引用。
上述逻辑是在 Native 层 GC 实现的,Java 层的 Reference 结构其实很简单:
public abstract class Reference<T> {
// Treated specially by GC. ART's ClassLinker::LinkFields() knows this is the
// alphabetically last non-static field.
volatile T referent;
final ReferenceQueue<? super T> queue;
...
下面是简单的测试代码:
private val referenceQueue = ReferenceQueue<MyTest>()
private var phantomReference: PhantomReference<MyTest>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val myTest = MyTest()
// 注意 phantomReference不能定义为局部变量 它自身也会被回收
phantomReference = PhantomReference(myTest, referenceQueue)
Log.i("PhantomReferenceTest", "myTest 尚未回收,其虚引用为:$phantomReference")
}
override fun onDestroy() {
super.onDestroy()
Log.i("PhantomReferenceTest", "onDestroy")
val reference = referenceQueue.poll()
if (reference != null) {
Log.i("PhantomReferenceTest", "myTest 被回收,其虚引用为:$reference")
}
}
class MyTest{}
这里有俩点需要注意:
- phantomReference 不能是局部变量,phantomReference 也会被回收,只有在 phantomReference 不为空的情况下,它的方法
enqueue
才能正常调用,虚引用自身才能正确保存。 - 有时为了方便测试,想要重写 MyTest 的
finalize()
方法,但是注意 Kotlin 不能重写该方法,虽然网上有提供覆盖的实现方式,但会导致各种引用的效果失效,如虚引用将不能被正确添加到 ReferenceQueue 中。
当前知道了哪些对象被回收,那么如何利用虚引用知道哪些对象没有被回收呢?
一种做法是创建 Map,其 Key 为 虚引用,Value 为参与被回收对象的 弱引用,GC 结束时,将被回收对象的虚引用从 Map 中移除掉,那么剩余的 Value 就是内存泄漏对象的弱引用,通过 Get 即可得到该对象。
LeakCanary 是如何实现的呢?
LeakCanary 实现原理
核心原理
这里不去分析 LeakCanary 复杂的源码,而是根据提炼出的 LeakCanary 的功能代码核心,实现出了简化版本 -- LeakCanaryLite
。
如果你读懂了 LeakCanaryLite
,那么就能明白 LeakCanary 是如何实现的内存泄漏的监控。为了简单,这里只提炼了检测 Activity 内存泄漏的逻辑。
个人觉得阅读源码可以学到优秀的框架设计、巧妙的实现思路,很重要。但博客更重要的是明确实现原理、传达核心思想,把代码贴出来,解析一通,不如亲自去源码里一探究极来的清晰明了。因此这里我没有粘贴源码,而是将核心原理以最精简的 Demo 的形式实现了出来。
package com.app.dixon.leakcanarydemo
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.os.Debug
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.lang.ref.Reference
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
object LeakCanaryLite {
private const val TAG = "LeakCanaryLite"
private lateinit var context: Application
// 保存Activity弱引用
private val list = mutableListOf<WeakReference<*>>()
// 用于延迟执行任务
private val handler = Handler(Looper.getMainLooper())
// 被回收的对象的弱引用会被加入到对列中
private val queue = ReferenceQueue<Any>()
fun install(context: Application) {
this.context = context
context.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
addWatch(activity)
}
})
}
private fun addWatch(activity: Activity) {
val watchReference = WeakReference(activity, queue) // 弱引用的对象也可以在被回收时将弱引用加入到指定queue中
list.add(watchReference)
Log.i(TAG, "Add Watch WeakReference:$watchReference")
// 空闲时执行
Looper.myQueue().addIdleHandler {
// 延迟5s执行
handler.postDelayed({
removeWeaklyReachableReferences()
runGc()
removeWeaklyReachableReferences()
runLeakTest()
}, 5000)
false
}
}
// 移除已经被回收的对象的弱引用 那么剩下的就是内存泄漏的对象
private fun removeWeaklyReachableReferences() {
var reference: Reference<*>? = queue.poll()
while (reference != null) {
Log.i(TAG, "Remove WeakReference:$reference")
list.remove(reference) // 移除已经回收的对象的弱引用
reference = queue.poll()
}
}
// 手动执行GC 因为JVM不一定在Activity.onDestroy后执行GC回收
private fun runGc() {
Runtime.getRuntime()
.gc()
try {
Thread.sleep(100)
} catch (e: InterruptedException) {
throw AssertionError()
}
System.runFinalization()
}
// 开始判断是否有泄漏,哪些对象泄漏了
private fun runLeakTest() {
if (list.isNotEmpty()) {
Log.i(TAG, "The memory leak objects are:")
for (reference in list) {
Log.i(TAG, "WeakReference is $reference and object is ${reference.get()}")
list.remove(reference) // 移除已经检测完的泄漏Activity
}
dumpHprof()
} else {
Log.i(TAG, "congratulations, no leaks")
}
}
// 导出堆转储文件
// LeakCanary 进一步分析了内存泄漏的点,这里为了简化逻辑直接导出该文件,然后使用 Android Profiler 分析。
private fun dumpHprof() {
// /storage/emulated/0/Android/data/包名/cache
Debug.dumpHprofData("${context.externalCacheDir}/LeakCanaryLite${System.currentTimeMillis()}.hprof")
Log.i(TAG, "Hprof 已保存")
}
}
核心流程是:
- 在 Appliction 中,通过
context.registerActivityLifecycleCallbacks
给每个 Activity 注册onDestroy
回调; - 回调里,创建 Activity 的弱引用,并关联 ReferenceQueue,同时将弱引用加入引用集合中;之后利用 Handler 创建一个 Idle、并且延迟的任务,用以分析内存泄漏;
- 分析任务开始,首先确保 GC 执行,并从引用集合中移除已经被回收对象的弱引用。移除完毕后,如果引用集合中不为空,那么说明这部分执行了 Destroy 和 GC 的 Activity 没有被正常释放,即存在内存泄漏。
- 确认存在内存泄漏,一方面可以通过弱引用获得泄漏的对象,另一方面,可以使用
Debug.dumpHprofData
获取当前的堆转储文件,以做进一步分析。
这里为了保留核心业务省略了 LeakCanary 后续的解析逻辑。
后续流程简而言之可以总结为:
- 将当前内存中存在的对象与 LeakCanary 抓到的泄漏对象一一比对,以确保确实发生了泄漏;
- 查找泄漏对象的最短引用链,包装后返回分析结果;
- 将分析结果通过通知展示出来。
LeakCanary 本身做了更多巧妙、复杂的处理,这不在本文的讨论之列。
利用堆转储、字节码分析内存泄漏
这节将结合上面的核心代码、最终导出的 .hprof 文件以及一个例子,分析并定位内存泄漏。
确保在 Application 中已经调用了 LeakCanaryLite.install
测试泄漏的代码如下:
class SecondActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
Thread {
Thread.sleep(20000)
}.start()
}
}
运行后输入如下日志:
I/LeakCanaryLite: Add Watch WeakReference:java.lang.ref.WeakReference@aa4a998
I/LeakCanaryLite: Remove WeakReference:java.lang.ref.WeakReference@aa4a998
I/LeakCanaryLite: congratulations, no leaks
发现没有内存泄漏,这是什么原因呢?
在当前文件下,工具栏选择 Tools -- Kotin -- Show Kotlin Bytecode,打开字节码文件。
阅读字节码文件有利于分析最终代码的结构。
实际上并不需要完全会读字节码文件,通过查询指令的解释可以明白字节码文件内的大多数意图。
这里提供一个用于查询字节码指令的链接:
https://www.cnblogs.com/xpwi/p/11360692.html
在字节码文件中,我们发现下面的一段代码:
L6
LINENUMBER 24 L6
NEW java/lang/Thread
DUP
GETSTATIC com/app/dixon/leakcanarydemo/SecondActivity$onCreate$1.INSTANCE : Lcom/app/dixon/leakcanarydemo/SecondActivity$onCreate$1;
CHECKCAST java/lang/Runnable
INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V
其中 onCreate$1 的部分字节码如下:
final class com/app/dixon/leakcanarydemo/SecondActivity$onCreate$1 implements java/lang/Runnable {
...
// access flags 0x19
public final static Lcom/app/dixon/leakcanarydemo/SecondActivity$onCreate$1; INSTANCE
...
所以说 com/app/dixon/leakcanarydemo/SecondActivity$onCreate$1
就是我们创建的匿名类 Runnable,这里匿名类的命名规则为:类$方法$第N个匿名类。
那么没有泄漏的原因就清楚了,原来编译器会自动优化代码,当 Runnable 本身没有操作外部类的成员时,会创建一个静态的 Runnable 对象压入栈。
接下来修改测试代码:
class SecondActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val nested = Nested()
Thread {
Thread.sleep(20000)
Log.e("LeakCanaryLite", "$nested")
}.start()
}
class Nested
}
传一个嵌套类进去,输入日志如下:
I/LeakCanaryLite: Add Watch WeakReference:java.lang.ref.WeakReference@aa4a998
I/LeakCanaryLite: Remove WeakReference:java.lang.ref.WeakReference@aa4a998
I/LeakCanaryLite: congratulations, no leaks
依然没有内存泄漏,继续查看字节码,可以发现,Create$1,也就是匿名 Runnable 对象只持有 nested 的引用:
final class com/app/dixon/leakcanarydemo/SecondActivity$onCreate$1 implements java/lang/Runnable {
...
// access flags 0x1010
final synthetic Lcom/app/dixon/leakcanarydemo/SecondActivity$Nested; $nested
...
}
同时 nested 本身也没有持有外部类,也就是 Activity 的引用,所以没有内存泄漏。
如果将嵌套类替换成内部类呢?
class SecondActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val inner = Inner()
Thread {
Thread.sleep(20000)
Log.e("LeakCanaryLite", "$inner")
}.start()
}
inner class Inner
}
日志如下:
I/LeakCanaryLite: Add Watch WeakReference:java.lang.ref.WeakReference@aa4a998
I/LeakCanaryLite: The memory leak objects are:
I/LeakCanaryLite: WeakReference is java.lang.ref.WeakReference@aa4a998 and object is com.app.dixon.leakcanarydemo.SecondActivity@a1945f
I/.leakcanarydem: hprof: heap dump "/storage/emulated/0/Android/data/com.app.dixon.leakcanarydemo/cache/LeakCanaryLite1631176984799.hprof" starting...
I/LeakCanaryLite: Hprof 已保存
这次泄漏了,查看字节码,可以发现 inner 对象持有外部类即 Activity 的引用,而 Runnable 又持有 inner 对象的引用:
public final class com/app/dixon/leakcanarydemo/SecondActivity$Inner {
...
// access flags 0x1010
final synthetic Lcom/app/dixon/leakcanarydemo/SecondActivity; this$0
}
final class com/app/dixon/leakcanarydemo/SecondActivity$onCreate$1 implements java/lang/Runnable {
...
// access flags 0x1010
final synthetic Lcom/app/dixon/leakcanarydemo/SecondActivity$Inner; $inner
...
}
通过阅读字节码,我们知道了泄漏的根本原因,并据此造出了我们想要的内存泄漏。
现在我们假设不知道哪里泄漏,通过堆转储文件来分析。
首先找到我们导出的堆转储文件,通过 AS 打开:
双击,AS 自动打开 Android Profile 进行分析。
正常使用 Android Profile 的情况下,如果发生内存泄漏则 Leaks 一栏会提示当前的泄漏数,但是使用上例中导出的 .hprof 文件并没有该提示,猜测原因是 AS 只是拿到了某一时刻的堆转储数据,并不知道我们 SecondActiviy 已经关闭了,仅仅是通过 GC Root 了解到该 SecondActivity 还被引用着,所以无法做出 SecondActiviy 泄漏的判断。
我们知道 SecondActivity 已经关闭,但从上图可以看出 SecondActivity 仍然存活,因此通过查找他的引用者可以了解到它被 SecondActivity$onCreate$1 持有了,再查看字节码,即可知 SecondActivity$onCreate$1 是 Runnable,以及它在何时持有了 this$0。
总结
本文主要解析了 LeakCanaray 的核心实现原理,以及如何结合堆转储、字节码去定位内存泄漏。