LeakCanary 与内存泄漏定位

虚引用

在了解 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{}

这里有俩点需要注意:

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

核心流程是:

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

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

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

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容