LeakCanary 与内存泄漏定位

虚引用

在了解 LeakCanary 之前,先来了解下虚引用。

虚引用必须与 ReferenceQueue 一起使用,当 GC 准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的 ReferenceQueue 中。也就是说 ReferenceQueue 中的对象,就是被成功回收对象的虚引用。

上述逻辑是在 Native 层 GC 实现的,Java 层的 Reference 结构其实很简单:


public abstract class Reference {
    // Treated specially by GC. ART's ClassLinker::LinkFields() knows this is the
    // alphabetically last non-static field.
    volatile T referent;

    final ReferenceQueue queue;
    ...

下面是简单的测试代码:

private val referenceQueue = ReferenceQueue()
private var phantomReference: PhantomReference? = 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{}

这里有俩点需要注意:

  1. phantomReference 不能是局部变量,phantomReference 也会被回收,只有在 phantomReference 不为空的情况下,它的方法 enqueue 才能正常调用,虚引用自身才能正确保存。
  2. 有时为了方便测试,想要重写 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>()

    // 用于延迟执行任务
    private val handler = Handler(Looper.getMainLooper())

    // 被回收的对象的弱引用会被加入到对列中
    private val queue = ReferenceQueue()

    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 已保存")
    }
}

核心流程是:

  1. 在 Appliction 中,通过 context.registerActivityLifecycleCallbacks 给每个 Activity 注册 onDestroy 回调;
  2. 回调里,创建 Activity 的弱引用,并关联 ReferenceQueue,同时将弱引用加入引用集合中;之后利用 Handler 创建一个 Idle、并且延迟的任务,用以分析内存泄漏;
  3. 分析任务开始,首先确保 GC 执行,并从引用集合中移除已经被回收对象的弱引用。移除完毕后,如果引用集合中不为空,那么说明这部分执行了 Destroy 和 GC 的 Activity 没有被正常释放,即存在内存泄漏。
  4. 确认存在内存泄漏,一方面可以通过弱引用获得泄漏的对象,另一方面,可以使用 Debug.dumpHprofData 获取当前的堆转储文件,以做进一步分析。

这里为了保留核心业务省略了 LeakCanary 后续的解析逻辑。

后续流程简而言之可以总结为:

  1. 将当前内存中存在的对象与 LeakCanary 抓到的泄漏对象一一比对,以确保确实发生了泄漏;
  2. 查找泄漏对象的最短引用链,包装后返回分析结果;
  3. 将分析结果通过通知展示出来。

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. (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

正常使用 Android Profile 的情况下,如果发生内存泄漏则 Leaks 一栏会提示当前的泄漏数,但是使用上例中导出的 .hprof 文件并没有该提示,猜测原因是 AS 只是拿到了某一时刻的堆转储数据,并不知道我们 SecondActiviy 已经关闭了,仅仅是通过 GC Root 了解到该 SecondActivity 还被引用着,所以无法做出 SecondActiviy 泄漏的判断。

我们知道 SecondActivity 已经关闭,但从上图可以看出 SecondActivity 仍然存活,因此通过查找他的引用者可以了解到它被 SecondActivity$onCreate$1 持有了,再查看字节码,即可知 SecondActivity$onCreate$1 是 Runnable,以及它在何时持有了 this$0。

总结

本文主要解析了 LeakCanaray 的核心实现原理,以及如何结合堆转储、字节码去定位内存泄漏。

你可能感兴趣的:(LeakCanary 与内存泄漏定位)