美文网首页Android
为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary

为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary

作者: 彭旭锐 | 来源:发表于2022-08-23 12:44 被阅读0次

    前言

    LeakCanary 是我们非常熟悉内存泄漏检测工具,它能够帮助开发者非常高效便捷地检测 Android 中常见的内存泄漏。在各大厂自研的内存泄漏检测框架(如腾讯 Matrix 和快手 Koom)的帮助文档中,也会引述 LeakCanary 原理分析。

    不吹不黑,LeakCanary 源码中除了实现内存泄漏的监控方案外,还有非常多值得学习的编程技巧,只有沉下心去阅读的人才能够真正体会到。在这篇文章里,我将带你从入门开始掌握 LeakCanary 的使用场景以及使用方法,再介绍 LeakCanary 的工作流程和高级用法,最后通过源码解析深入理解原理。本文示例程序已上传到 Github: DemoHall · HelloLeakCanary ,有用请给 Star 支持,谢谢。

    提示: 本文源码分析基于 2022 年 4 月发布的 LeakCanary 2.9.1。


    本文原理分析涉及的 Java 虚拟机内存管理基础:

    本文源码分析涉及的 Android 原理基础:


    学习路线图:


    1. 认识 LeakCanary

    1.1 什么是内存泄漏?

    内存泄露(Memory Leaks)指不再使用的对象或数据没有被回收,随着内存泄漏的堆积,应用性能会逐渐变差,甚至发生 OOM 奔溃。在 Android 应用中的内存泄漏可以分为 2 类:

    • Java 内存泄露: 不再使用的对象被生命周期更长的 GC Root 引用,无法被判定为垃圾对象而导致内存泄漏(LeakCanary 只能监控 Java 内存泄漏);
    • Native 内存泄露: Native 内存没有垃圾回收机制,未手动回收导致内存泄漏。

    1.2 为什么要使用 LeakCanary?

    LeakCanray 是 Square 开源的 Java 内存泄漏分析工具,用于在实验室阶段检测 Android 应用中常见中的内存泄漏。

    LeakCanary 的特点或优势在于提前预判出 Android 应用中最常见且影响较大的内存泄漏场景,并对此做针对性的监测手段。 这使得 LeakCanary 相比于其他排查内存泄漏的方案(如分析 OOM 异常时的堆栈日志、MAT 分析工具)更加高效。因为当内存泄漏堆积而内存不足时,应用可能从任何一次无关紧要的内存分配中抛出 OOM,堆栈日志只能体现最后一次内存分配的堆栈信息,而无法体现出导致发生 OOM 的主要原因。

    目前,LeakCanary 支持以下五种 Android 场景中的内存泄漏监测:

    • 1、已销毁的 Activity 对象(进入 DESTROYED 状态);
    • 2、已销毁的 Fragment 对象和 Fragment View 对象(进入 DESTROYED 状态);
    • 3、已清除的的 ViewModel 对象(进入 CLEARED 状态);
    • 4、已销毁的的 Service 对象(进入 DESTROYED 状态);
    • 5、已从 WindowManager 中移除的 RootView 对象;

    1.3 LeakCanary 怎么实现内存泄漏监控?

    LeakCanary 通过以下 2 点实现内存泄漏监控:

    • 1、在 Android Framework 中注册无用对象监听: 通过全局监听器或者 Hook 的方式,在 Android Framework 上监听 Activity 和 Service 等对象进入无用状态的时机(例如在 Activity#onDestroy() 后,产生一个无用 Activity 对象);
    • 2、利用引用对象可感知对象垃圾回收的机制判定内存泄漏: 为无用对象包装弱引用,并在一段时间后(默认为五秒)观察弱引用是否如期进入关联的引用队列,是则说明未发生泄漏,否则说明发生泄漏(无用对象被强引用持有,导致无法回收,即泄漏)。

    详细的源码分析下文内容。


    2. 理解 LeakCanary 的工作流程

    虽然 LeakCanary 的使用方法非常简单,但是并不意味着 LeakCanary 的工作流程也非常简单。在了解 LeakCanary 的使用方法和深入 LeakCanary 的源码之前,我们先理解 LeakCanary 的核心工作流程,我将其概括为以下 5 个阶段:

    • 1、注册无用对象监听: 在 Android Framework 中注册监听器,感知五种 Android 内存泄漏场景中产生无用对象的时机(例如在 Activity#onDestroy() 后,产生一个无用 Activity 对象);
    • 2、监控内存泄漏: 为无用对象关联弱引用对象,如果一段时间后引用对象没有按预期进入引用队列,则认为对象发生内存泄漏。由于分析堆快照是耗时工作,所以 LeakCanary 不会每次发现内存泄漏对象都进行分析工作,而是内存泄漏对象计数到达阈值才会触发分析工作。在计数未到达阈值的过程中,LeakCanary 会发送一条系统通知,你也可以点击该通知提前触发分析工作;

    收集过程中的系统通知消息

    !](https://img.haomeiwen.com/i10107787/ce29f36a9f3fbe92.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    提示: LeakCanary 为不同的 App 状态设置了不同默认阈值:App 可见时阈值为 5 个泄漏对象,App 不可见时阈值为 1 个泄漏对象。举个例子,如果 App 在前台可见并且已经收集了 4 个泄漏的对象,此时 App 退到后台,LeakCanary 会在五秒后触发分析工作。

    • 3、Java Heap Dump: 当泄漏对象计数达到阈值时,会触发 Java Heap Dump 并生成 .hprof 文件存储到文件系统中。Heap Dump 的过程中会锁堆,会使应用冻结一段时间;

    Heap Dump 过程中的全局对话框

    • 4、分析堆快照: LeakCanary 会根据应用的依赖项,选择 WorkManager 多进程、WorkManager 异步任务或 Thread 异步任务其中一种策略来执行分析(例如,LeakCanary 会检查应用有 leakcanary-android-process 依赖项,才会使用 WorkManager 多进程策略)。分析过程 LeakCanary 使用 Shark 分析 .hprof 文件,替换了 LeakCanary 1.0 使用的 haha
    • 5、输出分析报告: 当分析工作完成后,LeakCanary 会在 Logcat 打印分析结果,也会发送一条系统通知消息。点击通知消息可以跳转到可视化分析报告页面,也可以点击 LeakCanary 生成的桌面快捷方式进入。

    分析结束后的系统通知消息

    新增的启动图标

    可视化分析报告

    至此,LeakCanary 一次内存泄漏分析工作流程执行完毕。


    3. LeakCanary 的基本用法

    这一节,我们来介绍 LeakCanary 的基础用法。

    3.1 将 LeakCanary 添加到项目中

    在 build.gradle 中添加 LeakCanary 依赖,此外不需要调用任何初始化 API(LeakCanary 内部默认使用了 ContentProvider 实现无侵入初始化)。另外,因为 LeakCanary 是只在实验室环境使用的工具,所以这里要记得使用 debugImplementation 依赖配置。

    build.gradle

    dependencies {
        // debugImplementation because LeakCanary should only run in debug builds.
        debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
    }
    

    3.2 手动初始化 LeakCanary

    LeakCanary 2.0 默认采用了 ContentProvider 机制实现了无侵入初始化,为了给予开发者手动初始化 LeakCanary 的可能性,LeakCanary 在 ContentProvider 中设置了布尔值开关:

    AndroidManifest.xml

    <application>
        <provider
            android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
            android:authorities="${applicationId}.leakcanary-installer"
            android:enabled="@bool/leak_canary_watcher_auto_install"
            android:exported="false"/>
    </application>
    

    开发者只需要在资源文件里覆写 @bool/eak_canary_watcher_auto_install 布尔值来关闭自动初始化,并在合适的时机手动调用 AppWatcher#manualInstall

    values.xml

    <resources>
        <bool name="leak_canary_watcher_auto_install">false</bool>
    </resources>
    

    3.3 自定义 LeakCanary 配置

    LeakCanary 为开发者提供了便捷的配置 API,并且这个配置 API 在初始化前后都允许调用。

    示例程序

    // Java 语法
    LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
        .retainedVisibleThreshold(3)
        .build();
    LeakCanary.setConfig(config);
    
    // Kotlin 语法
    LeakCanary.config = LeakCanary.config.copy(
        retainedVisibleThreshold = 3
    )
    

    以下用一个表格总结 LeakCanary 主要的配置项:

    配置项 描述 默认值
    dumpHeap: Boolean Heap Dump 分析开关 true
    dumpHeapWhenDebugging: Boolean 调试时 Heap Dump 分析开关 false
    retainedVisibleThreshold: Int App 可见时泄漏计数阈值 5
    objectInspectors: List<ObjectInspector> 对象检索器 AndroidObjectInspectors.appDefaults
    computeRetainedHeapSize: Boolean 是否计算泄漏内存空间 true
    maxStoredHeapDumps: Int 最大堆快照存储数量 7
    requestWriteExternalStoragePermission: Boolean 是否请求文件存储权限 true
    leakingObjectFinder: LeakingObjectFinder 引用链分析器 KeyedWeakReferenceFinder
    heapDumper: HeapDumper Heap Dump 执行器 Debug.dumpHprofData
    eventListeners: List<EventListener> 事件监听器 多个内部监听器

    4. 解读 LeakCanary 分析报告

    内存泄漏分析报告是 LeakCanary 所有监控和分析工作后输出的目标产物,要根据修复内存泄漏,首先就要求开发者能够读懂 LeakCanary 的分析报告。我将 LeakCanary 的分析报告总结为以下 4 个要点:

    4.1 泄漏对象的引用链

    泄漏对象的引用链是分析报告的核心信息,LeakCanary 会收集泄漏对象到 GC Root 的完整引用链信息。例如,以下示例程序在 static 变量中持有一个 Helper 对象,当 Helper 被期望被垃圾回收时用 AppWatcher 监测该对象,如果未按预期被回收,则会输出以下分析报告:

    示例程序

    class Helper {
    }
    
    class Utils {
        public static Helper helper = new Helper();
    }
    
    // Helper 无用后监测
    AppWatcher.objectWatcher.watch(helper, "Helper is no longer useful")
    

    Logcat 日志

    ┬───
    │ GC Root: Local variable in native code
    │
    ├─ dalvik.system.PathClassLoader instance
    │    ↓ PathClassLoader.runtimeInternalObjects // 表示 PathClassLoader 中的 runtimeInternalObjects 字段,它是一个 Object 数组
    ├─ java.lang.Object[] array
    │    ↓ Object[].[43] // 表示 Object 数组的第 43 位,它是一个 Utils 类型引用
    ├─ com.example.Utils class
    │    ↓ static Utils.helper // 表示 Utils 的 static 字段,它是一个 Helper 类型引用
    ╰→ java.example.Helper
    

    解释一下其中的符号:

    • 代表一个 Java 对象;
    • │ ↓ 代表一个 Java 引用,关联的实际对象在下一行;
    • ╰→ 代表泄漏的对象,即 AppWatcher.objectWatcher.watch() 直接监控的对象。

    4.2 按引用链签名分组

    用减少重复的排查工作,LeakCanary 会将相同问题重复触发的内存泄漏进行分组,分组方法是按引用链的签名。引用链签名是对引用链上经过的每个对象的类型拼接后取哈希值,既然应用链完全相同,就没必要重复排查了。

    例如,对于泄漏对象 instance,对应的泄漏签名计算公式如下:

    Logcat 日志

    ...
    │  
    ├─ com.example.leakcanary.LeakingSingleton class
    │    Leaking: NO (a class is never leaking)
    │    ↓ static LeakingSingleton.leakedViews
    │                              ~~~~~~~~~~~
    ├─ java.util.ArrayList instance
    │    Leaking: UNKNOWN
    │    ↓ ArrayList.elementData
    │                ~~~~~~~~~~~
    ├─ java.lang.Object[] array
    │    Leaking: UNKNOWN
    │    ↓ Object[].[0]
    │               ~~~
    ├─ android.widget.TextView instance
    │    Leaking: YES (View.mContext references a destroyed activity)
    

    对应的签名计算公式

    val leakSignature = sha1Hash(
        "com.example.leakcanary.LeakingSingleton.leakedView" +
        "java.util.ArrayList.elementData" +
        "java.lang.Object[].[x]"
    )
    println(leakSignature)
    // dbfa277d7e5624792e8b60bc950cd164190a11aa
    

    4.3 使用 ~~~ 标记怀疑对象

    为了提高排查内存泄漏的效率,LeakCanary 会自动帮助我们根据对象的生命周期信息或状态信息缩小排查范围,排除原本就具有全局生命周期的对象,剩下的用 ~~~ 下划线标记为怀疑对象。

    例如,在以下内存泄漏报告中,ExampleApplication 对象被 FontsContract.sContext 静态变量持有,表面看起来是 sContext 静态变量导致内存泄漏。其实不是,因为 ExampleApplication 的生命周期是全局的且永远不会被垃圾回收的,所以内存泄漏的根本原因一定不是因为 sContext 持有 ExampleApplication 引起的,sContext 这条引用可以排除,所以它不会用 ~~~ 下划线标记。

    4.4 按 Application Leaks 和 Library Leaks 分类

    为了提高排查内存泄漏的效率,LeakCanary 会自动将泄漏报告划分为 2 类:

    • Application Leaks: 应用层代码产生的内存泄漏,包括项目代码和第三方库代码;
    • Library Leaks: Android Framework 产生的内存泄漏,开发者几乎无法做什么,可以忽略。

    其实,Library Leaks 这个名词起得并不好,应该叫作 Framework Leaks。 小彭最初在阅读官方文档后,以为 Library Leaks 是只第三方库代码产生的内存泄漏,LeakCanary 还提到开发者对于 Library Leaks 几乎无法做什么,让我一度很好奇 LeakCanary 是如何定义二方库和三方库。最后还是通过源码才得知,Library Leaks 原来是指 Android Framework 中产生的内存泄漏,例如什么 TextView、InputMethodManager 之类的。

    Logcat 中的 Library Leak 标记

    ====================================
    HEAP ANALYSIS RESULT
    ====================================
    0 APPLICATION LEAKS
    
    ====================================
    1 LIBRARY LEAK
    
    ...
    ┬───
    │ GC Root: Local variable in native code
    │
    ...
    

    5. LeakCanary 的进阶用法

    5.1 使用 App Startup 初始化 LeakCanary

    LeakCanary 2.8 提供了对 Jetpack · App Startup 的支持。如果想使用 App Startup 初始化 LeakCanary,只需要替换为另一个依赖。不过,毕竟 LeakCanary 是主要在实验室环境使用的工具,这个优化的意义并不大。

    build.gradle

    dependencies {
        // 替换为另一个依赖
        // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
        debugImplementation 'com.squareup.leakcanary:leakcanary-android-startup:2.9.1'
    }
    

    对应的 App Startup 启动器源码:

    AppWatcherStartupInitializer.kt

    internal class AppWatcherStartupInitializer : Initializer<AppWatcherStartupInitializer> {
        override fun create(context: Context) = apply {
            val application = context.applicationContext as Application
            AppWatcher.manualInstall(application)
        }
        override fun dependencies() = emptyList<Class<out Initializer<*>>>()
    }
    

    5.2 在子进程执行 LeakCanary 分析工作

    由于 LeakCanary 分析堆快照的过程存在一定的内存消耗,整个分析过程一般会持续几十秒,对于一些性能差的机型会造成明显的卡顿甚至 ANR。为了优化内存占用和卡顿问题,LeakCanary 2.8 提供了对多进程的支持。开发者只需要依赖 LeakCanary 的多进程依赖项,LeakCanary 会自动将分析工作转移到子进程中(基于 androidX.work.multiprocess):

    build.gradle

    dependencies {
        // 官方文档对多进程功能的介绍有矛盾,经过测试,以下两个依赖都需要
        debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
        debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:2.9.1'
    }
    

    同时,开发者需要在自定义 Application 中检查当前进程信息,避免在 LeakCanary 的子进程中执行不必要的初始化操作:

    ExampleApplication.kt

    class ExampleApplication : Application() {
        override fun onCreate() {
            if (LeakCanaryProcess.isInAnalyzerProcess(this)) {
                return
            }
            super.onCreate()
            // normal init goes here, skipped in :leakcanary process.
        }
    }
    

    Logcat 进程选项

    Logcat 日志

    LeakCanary: Enqueuing heap analysis for /storage/emulated/0/Download/leakcanary-com.pengxr.helloleakcanary/2022-08-22_19-54-24_331.hprof on WorkManager remote worker
    

    5.3 使用快手 Koom 加快 Dump 速度

    LeakCanary 默认的 Java Heap Dump 使用的是 Debug.dumpHprofData() ,在 Dump 的过程中会有较长时间的应用冻结时间。 快手技术团队在开源框架 Koom 中提出了优化方案:利用 Copy-on-Write 思想,fork 子进程再进行 Heap Dump 操作。

    LeakCanary 配置项可以修改 Heap Dump 执行器,示例程序如下:

    示例程序

    // 依赖: 
    debugImplementation "com.kuaishou.koom:koom-java-leak:2.2.0"
    
    // 使用默认配置初始化 Koom
    DefaultInitTask.init(application)
    // 自定义 LeakCanary 配置
    LeakCanary.config = LeakCanary.config.copy(
        // 自定义 Heap Dump 执行器
        heapDumper = {
            ForkJvmHeapDumper.getInstance().dump(it.absolutePath)
        }
    )
    

    Logcat 日志对比

    // 使用默认的 Debug.dumpHprofData() 的日志
    helloleakcanar: hprof: heap dump "/storage/emulated/0/Download/leakcanary-com.pengxr.helloleakcanary/2022-08-22_18-47-28_674.hprof" starting...
    helloleakcanar: hprof: heap dump completed (34MB) in 1.552s objects 549530 objects with stack traces 0
    LeakCanary: Enqueuing heap analysis for /storage/emulated/0/Download/leakcanary-com.pengxr.helloleakcanary/2022-08-22_19-58-13_310.hprof on WorkManager remote worker
    ...
    
    // 使用快手 Koom Heap Dump 的日志
    OOMMonitor_ForkJvmHeapDumper: dump /storage/emulated/0/Download/leakcanary-com.pengxr.helloleakcanary/2022-08-22_19-54-24_331.hprof
    OOMMonitor_ForkJvmHeapDumper: before suspend and fork.
    OOMMonitor_ForkJvmHeapDumper: dump true, notify from pid 8567
    LeakCanary: Enqueuing heap analysis for /storage/emulated/0/Download/leakcanary-com.pengxr.helloleakcanary/2022-08-22_19-54-24_331.hprof on WorkManager remote worker
    ...
    

    看一眼 Koom 源码:

    ForkJvmHeapDumper.java

    public synchronized boolean dump(String path) {
        boolean dumpRes = false;
        int pid = suspendAndFork();
        if (pid == 0) {
            // Child process
            Debug.dumpHprofData(path);
            exitProcess();
        } else if (pid > 0) {
            // Parent process
            dumpRes = resumeAndWait(pid);
        }
        return dumpRes;
    }
    
    private native void nativeInit();
    private native int suspendAndFork();
    private native boolean resumeAndWait(int pid);
    private native void exitProcess();
    

    5.4 自定义标记引用信息

    LeakCanary 配置项可以自定义 ObjectInspector 对象检索器,在引用链上的节点中标记必要的信息和状态。标记信息会显示在分析报告中,并且会影响报告中的提示。

    • notLeakingReasons 标记: 标记非泄漏原因后,节点为 NOT_LEAKING 状态,并在分析报告中会显示 Leaking: NO (notLeakingReasons)
    • leakingReasons 标记: 标记泄漏原因后,节点为 LEAKING 状态,在分析报告中会显示 Leaking: YES (leakingReasons)
    • 缺省: 节点为 UNKNOWN 状态,在分析报告中会显示 Leaking: UNKNOWN

    示例程序如下:

    示例程序

    // 自定义 LeakCanary 配置
    LeakCanary.config = LeakCanary.config.copy(
        // 自定义对象检索器
        objectInspectors = LeakCanary.config.objectInspectors + ObjectInspector { reporter ->
            // reporter.notLeakingReasons += "非泄漏原因"
            // reporter.leakingReasons += "泄漏原因"
        } + AppSingletonInspector(
            // 标记全局类的类名即可
        )
    )
    

    另外,引用链 LEAKING 节点以后到第一个 NOT_LEAKING 节点中间的节点,才会用 ~~~ 下划线标记为怀疑对象。例如:


    6. LeakCanary 实现原理分析

    使用一张示意图表示 LeakCanary 的基本架构:

    6.1 LeakCanary 如何实现自动初始化?

    旧版本的 LeakCanary 需要在 Application 中调用相关初始化 API,而在 LeakCanary v2 版本中却不再需要手动初始化,为什么呢?—— 这是因为 LeakCanary 利用了 ContentProvider 的初始化机制来间接调用初始化 API。

    ContentProvider 的常规用法是提供内容服务,而另一个特殊的用法是提供无侵入的初始化机制,这在第三方库中很常见,Jetpack 中提供的轻量级初始化框架 App Startup 也是基于 ContentProvider 的方案。

    MainProcessAppWatcherInstaller.kt

    internal class MainProcessAppWatcherInstaller : ContentProvider() {
        override fun onCreate(): Boolean {
            // 初始化 LeakCanary
            val application = context!!.applicationContext as Application
            AppWatcher.manualInstall(application)
            return true
        }
        ...
    }
    

    6.2 LeakCanary 初始化过程分析

    LeakCanary 的初始化工程可以概括为 2 项内容:

    • 1、初始化 LeakCanary 内部分析引擎;
    • 2、在 Android Framework 上注册五种 Android 泄漏场景的监控。

    AppWathcer.kt

    // LeakCanary 初始化 API
    @JvmOverloads
    fun manualInstall(
        application: Application,
        retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
        watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
    ) {
        checkMainThread()
        ...
        // 初始化 InternalLeakCanary 内部引擎 (已简化为等价代码,后文会提到)
        InternalLeakCanary(application)
        // 注册五种 Android 泄漏场景的监控 Hook 点
        watchersToInstall.forEach {
            it.install()
        }
    }
    
    fun appDefaultWatchers(
        application: Application,
        reachabilityWatcher: ReachabilityWatcher = objectWatcher
    ): List<InstallableWatcher> {
        // 对应 5 种 Android 泄漏场景(后文具体分析)
        return listOf(
            ActivityWatcher(application, reachabilityWatcher),
            FragmentAndViewModelWatcher(application, reachabilityWatcher),
            RootViewWatcher(reachabilityWatcher),
            ServiceWatcher(reachabilityWatcher)
        )
    }
    

    下面展开具体分析:


    初始化内容 1 - 初始化 LeakCanary 内部分析引擎: 创建 HeapDumpTrigger 触发器,并在 Android Framework 上注册前后台切换监听、前台 Activity 监听和 ObjectWatcher 的泄漏监听。

    InternalLeakCanary.kt

    override fun invoke(application: Application) {
        _application = application
    
        // 1. 检查是否运行在 debug 构建变体,否则抛出异常
        checkRunningInDebuggableBuild()
    
        // 2. 注册泄漏回调,在 ObjectWathcer 判定对象发生泄漏会后回调 onObjectRetained() 方法
        AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
    
        // 3. 垃圾回收触发器(用于调用 Runtime.getRuntime().gc())
        val gcTrigger = GcTrigger.Default
        // 4. 配置提供器
        val configProvider = { LeakCanary.config }
        // 5. (主角) 创建 HeapDump 触发器
        heapDumpTrigger = HeapDumpTrigger(...)
    
        // 6. App 前后台切换监听
        application.registerVisibilityListener { applicationVisible ->
            this.applicationVisible = applicationVisible
            heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
        }
        // 7. 前台 Activity 监听(用于发送 Heap Dump 进行中的全局 Toast)
        registerResumedActivityListener(application)
    
        // 8. 增加可视化分析报告的桌面快捷入口
        addDynamicShortcut(application)
    }
    
    override fun onObjectRetained() = scheduleRetainedObjectCheck()
    
    fun scheduleRetainedObjectCheck() {
        heapDumpTrigger.scheduleRetainedObjectCheck()
    }
    

    HeapDumpTrigger.kt

    // App 前后台切换状态变化回调
    fun onApplicationVisibilityChanged(applicationVisible: Boolean) {
        if (applicationVisible) {
            // App 可见
            applicationInvisibleAt = -1L
        } else {
            // App 不可见
            applicationInvisibleAt = SystemClock.uptimeMillis()
            scheduleRetainedObjectCheck(delayMillis = AppWatcher.retainedDelayMillis)
        } 
    }
    
    fun scheduleRetainedObjectCheck(delayMillis: Long = 0L) {
        // 已简化:源码此处使用时间戳拦截,避免重复 postDelayed
        backgroundHandler.postDelayed({
            checkScheduledAt = 0
            checkRetainedObjects()
        }, delayMillis)
    }
    

    初始化内容 2 - 在 Android Framework 中注入对五种 Android 泄漏场景的监控: 实现在对象的使用生命周期结束后,自动将对象交给 ObjectWatcher 进行监控。

    以下为 5 种 Android 泄漏场景的监控原理分析:

    • 1、Activity 监控: 通过 Application#registerActivityLifecycleCallbacks(…) 接口监听 Activity#onDestroy 事件,将当前 Activity 对象交给 ObjectWatcher 监控;

    ActivityWatcher.kt

    private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
        override fun onActivityDestroyed(activity: Activity) {
            // reachabilityWatcher 即 ObjectWatcher
            reachabilityWatcher.expectWeaklyReachable(activity /*被监控对象*/, "${activity::class.java.name} received Activity#onDestroy() callback")
        }
    }
    
    • 2、Fragment 与 Fragment View 监控: 通过 FragmentAndViewModelWatcher 实现,首先是通过 Application#registerActivityLifecycleCallbacks(…) 接口监听 Activity#onCreate 事件,再通过 FragmentManager#registerFragmentLifecycleCallbacks(…) 接口监听 Fragment 的生命周期:

    FragmentAndViewModelWatcher.kt

    // fragmentDestroyWatchers 是一个 Lambda 表达式数组
    // 对应原生、AndroidX 和 Support 三个版本 Fragment 的 Hook 工具
    private val fragmentDestroyWatchers: List<(Activity) -> Unit> = 略...
    
    private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
        override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
            for (watcher in fragmentDestroyWatchers) {
                // 最终调用到下文的 invokde() 方法
                watcher(activity)
            }
        }
    }
    

    以 AndroidX Fragment 为例:

    AndroidXFragmentDestroyWatcher.kt

    override fun invoke(activity: Activity) {
        // 这里在 Activity#onCreate 状态执行:
        if (activity is FragmentActivity) {
            val supportFragmentManager = activity.supportFragmentManager
            // 注册 Fragment 生命周期监听
            supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
            // 注册 Activity 级别 ViewModel Hook
            ViewModelClearedWatcher.install(activity, reachabilityWatcher)
        }
    }
    
    private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
    
        override fun onFragmentCreated(fm: FragmentManager, fragment: Fragment, savedInstanceState: Bundle?) {
            // 注册 Fragment 级别 ViewModel Hook
            ViewModelClearedWatcher.install(fragment, reachabilityWatcher)
        }
    
        override fun onFragmentViewDestroyed(fm: FragmentManager, fragment: Fragment) {
            // reachabilityWatcher 即 ObjectWatcher
            reachabilityWatcher.expectWeaklyReachable(fragment.view /*被监控对象*/, "${fragment::class.java.name} received Fragment#onDestroyView() callback " + "(references to its views should be cleared to prevent leaks)")
        }
    
        override fun onFragmentDestroyed(fm: FragmentManager, fragment: Fragment) {
            // reachabilityWatcher 即 ObjectWatcher
            reachabilityWatcher.expectWeaklyReachable(fragment /*被监控对象*/, "${fragment::class.java.name} received Fragment#onDestroy() callback")
        }
    }
    
    • 3、ViewModel 监控: 由于 Android Framework 未提供设置 ViewModel#onClear() 全局监听的方法,所以 LeakCanary 是通过 Hook 的方式实现。即:在 Activity#onCreate 和 Fragment#onCreate 事件中实例化一个自定义ViewModel,在进入 ViewModel#onClear() 方法时,通过反射获取当前作用域中所有的 ViewModel 对象交给 ObjectWatcher 监控。

    ViewModelClearedWatcher.kt

    // ViewModel 的子类
    internal class ViewModelClearedWatcher(
        storeOwner: ViewModelStoreOwner,
        private val reachabilityWatcher: ReachabilityWatcher
    ) : ViewModel() {
    
        // 反射获取 ViewModelStore 中的 ViewModel 映射表,即可获取当前作用域所有 ViewModel 对象
        private val viewModelMap: Map<String, ViewModel>? = try {
            val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
            mMapField.isAccessible = true
            mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
        } catch (ignored: Exception) {
            null
        }
    
        override fun onCleared() {
            // 遍历当前作用域所有 ViewModel 对象
            viewModelMap?.values?.forEach { viewModel ->
                // reachabilityWatcher 即 ObjectWatcher
                reachabilityWatcher.expectWeaklyReachable(viewModel /*被监控对象*/, "${viewModel::class.java.name} received ViewModel#onCleared() callback")
            }
        }
    
        companion object {
            // 直接在 storeOwner 作用域实例化 ViewModelClearedWatcher 对象
            fun install(storeOwner: ViewModelStoreOwner, reachabilityWatcher: ReachabilityWatcher) {
                val provider = ViewModelProvider(storeOwner, object : Factory {
                    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
                        ViewModelClearedWatcher(storeOwner, reachabilityWatcher) as T
                })
                provider.get(ViewModelClearedWatcher::class.java)
            }
        }
    }
    
    • 4、Service 监控: 由于 Android Framework 未提供设置 Service#onDestroy() 全局监听的方法,所以 LeakCanary 是通过 Hook 的方式实现的。

    Service 监控这部分源码比较复杂了,需要通过 2 步 Hook 来实现:

    • 1、Hook 主线程消息循环的 mH.mCallback 回调,监听其中的 STOP_SERVICE 消息,将即将 Destroy 的 Service 对象暂存起来(由于 ActivityThread.H 中没有 DESTROY_SERVICE 消息,所以不能直接监听到 onDestroy() 事件,需要第 2 步);
    • 2、使用动态代理 Hook AMS 与 App 通信的的 IActivityManager Binder 对象,代理其中的 serviceDoneExecuting() 方法,视为 Service#onDestroy() 的执行时机,拿到暂存的 Service 对象交给 ObjectWatcher 监控。

    源码摘要如下:

    ServiceWatcher.kt

    private var uninstallActivityThreadHandlerCallback: (() -> Unit)? = null
    
    // 暂存即将 Destroy 的 Service
    private val servicesToBeDestroyed = WeakHashMap<IBinder, WeakReference<Service>>()
    
    override fun install() {
        // 1. Hook mH.mCallback
        swapActivityThreadHandlerCallback { mCallback /*原对象*/ ->
            // uninstallActivityThreadHandlerCallback:用于取消 Hook
            uninstallActivityThreadHandlerCallback = {
                swapActivityThreadHandlerCallback {
                    mCallback
                }
            }
            // 新对象(lambda 表达式的末行就是返回值)
            Handler.Callback { msg ->
                // 1.1 Service#onStop() 事件
                if (msg.what == STOP_SERVICE) {
                    val key = msg.obj as IBinder
                    // 1.2 activityThreadServices:反射获取 ActivityThread mServices 映射表 <IBinder, CreateServiceData>
                    activityThreadServices[key]?.let {
                        // 1.3 暂存即将 Destroy 的 Service
                        servicesToBeDestroyed[token] = WeakReference(service)
                    }
                }
                // 1.4 继续执行 Framework 原有逻辑
                mCallback?.handleMessage(msg) ?: false
            }
        }
        // 2. Hook AMS IActivityManager
        swapActivityManager { activityManagerInterface, activityManagerInstance /*原对象*/ ->
            // uninstallActivityManager:用于取消 Hook
            uninstallActivityManager = {
                swapActivityManager { _, _ ->
                    activityManagerInstance
                }
            }
            // 新对象(lambda 表达式的末行就是返回值)
            Proxy.newProxyInstance(activityManagerInterface.classLoader, arrayOf(activityManagerInterface)) { _, method, args ->
                // 2.1 代理 serviceDoneExecuting() 方法
                if (METHOD_SERVICE_DONE_EXECUTING == method.name) {
                    // 2.2 取出暂存的即将 Destroy 的 Service
                    val token = args!![0] as IBinder
                    if (servicesToBeDestroyed.containsKey(token)) {
                        servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
                            // 2.3 交给 ObjectWatcher 监控
                            serviceWeakReference.get()?.let { service ->
                                reachabilityWatcher.expectWeaklyReachable(service /*被监控对象*/, "${service::class.java.name} received Service#onDestroy() callback")
                            }
                        }
                    }
                }
                // 2.4 继续执行 Framework 原有逻辑
                method.invoke(activityManagerInstance, *args)
            }
        }
    }
    
    override fun uninstall() {
        // 关闭 mH.mCallback 的 Hook
        uninstallActivityManager?.invoke()
        uninstallActivityThreadHandlerCallback?.invoke()
        uninstallActivityManager = null
        uninstallActivityThreadHandlerCallback = null
    }
    
    // 使用反射修改 ActivityThread 的主线程消息循环的 mH.mCallback
    // swap 是一个 lambda 表达式,参数为原对象,返回值为注入的新对象
    private fun swapActivityThreadHandlerCallback(swap: (Handler.Callback?) -> Handler.Callback?) {
        val mHField = activityThreadClass.getDeclaredField("mH").apply { isAccessible = true }
        val mH = mHField[activityThreadInstance] as Handler
    
        val mCallbackField = Handler::class.java.getDeclaredField("mCallback").apply { isAccessible = true }
        val mCallback = mCallbackField[mH] as Handler.Callback?
        // 将 swap 的返回值作为新对象,实现 Hook
        mCallbackField[mH] = swap(mCallback)
    }
    
    // 使用反射修改 AMS 与 App 通信的 IActivityManager Binder 对象
    // swap 是一个 lambda 表达式,参数为 IActivityManager 的 Class 对象和接口原实现对象,返回值为注入的新对象
    private fun swapActivityManager(swap: (Class<*>, Any) -> Any) {
        val singletonClass = Class.forName("android.util.Singleton")
        val mInstanceField = singletonClass.getDeclaredField("mInstance").apply { isAccessible = true }
    
        val singletonGetMethod = singletonClass.getDeclaredMethod("get")
    
        val (className, fieldName) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            "android.app.ActivityManager" to "IActivityManagerSingleton"
        } else {
            "android.app.ActivityManagerNative" to "gDefault"
        }
    
        val activityManagerClass = Class.forName(className)
        val activityManagerSingletonField = activityManagerClass.getDeclaredField(fieldName).apply { isAccessible = true }
        val activityManagerSingletonInstance = activityManagerSingletonField[activityManagerClass]
    
        // Calling get() instead of reading from the field directly to ensure the singleton is
        // created.
        val activityManagerInstance = singletonGetMethod.invoke(activityManagerSingletonInstance)
    
        val iActivityManagerInterface = Class.forName("android.app.IActivityManager")
        // 将 swap 的返回值作为新对象,实现 Hook
        mInstanceField[activityManagerSingletonInstance] = swap(iActivityManagerInterface, activityManagerInstance!!)
    }
    
    • 5、RootView 监控: 由于 Android Framework 未提供设置全局监听 RootView 从 WindowManager 中移除的方法,所以 LeakCanary 是通过 Hook 的方式实现的,这一块是通过 squareup 另一个开源库 curtains 实现的。

    RootView 监控这部分源码也比较复杂了,需要通过 2 步 Hook 来实现:

    • 1、Hook WMS 服务内部的 WindowManagerGlobal.mViews RootView 列表,获取 RootView 新增和移除的时机;
    • 2、检查 View 对应的 Window 类型,如果是 Dialog 或 DreamService 等类型,则在注册 View#addOnAttachStateChangeListener() 监听,在其中的 onViewDetachedFromWindow() 回调中将 View 对象交给 ObjectWatcher 监控。

    LeakCanary 源码摘要如下:

    RootViewWatcher.kt

    override fun install() {
        // 1. 注册 RootView 监听
        Curtains.onRootViewsChangedListeners += listener
    }
    
    private val listener = OnRootViewAddedListener { rootView ->
        val trackDetached = when(rootView.windowType) {
        PHONE_WINDOW -> {
            when (rootView.phoneWindow?.callback?.wrappedCallback) {
                // Activity 类型已经在 ActivityWatcher 中监控了,不需要重复监控
                is Activity -> false
                is Dialog -> {
                    // leak_canary_watcher_watch_dismissed_dialogs:Dialog 监控开关
                    val resources = rootView.context.applicationContext.resources
                    resources.getBoolean(R.bool.leak_canary_watcher_watch_dismissed_dialogs)
                }
                // DreamService 屏保等
                else -> true
            }
        }
        POPUP_WINDOW -> false
        TOOLTIP, TOAST, UNKNOWN -> true
        }
        if (trackDetached) {
            // 2. 注册 View#addOnAttachStateChangeListener 监听
            rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
                val watchDetachedView = Runnable {
                    // 3. 交给 ObjectWatcher 监控
                    reachabilityWatcher.expectWeaklyReachable(rootView /*被监控对象*/ , "${rootView::class.java.name} received View#onDetachedFromWindow() callback")
                }
    
                override fun onViewAttachedToWindow(v: View) {
                    mainHandler.removeCallbacks(watchDetachedView)
                }
    
                override fun onViewDetachedFromWindow(v: View) {
                    mainHandler.post(watchDetachedView)
                }
            })
        }
    }
    

    curtains 源码摘要如下:

    RootViewsSpy.kt

    private val delegatingViewList = object : ArrayList<View>() {
        // 重写 ArrayList#add 方法
        override fun add(element: View): Boolean {
            // 回调
            listeners.forEach { it.onRootViewsChanged(element, true) }
            return super.add(element)
        }
    
        // 重写 ArrayList#removeAt 方法
        override fun removeAt(index: Int): View {
            // 回调
            val removedView = super.removeAt(index)
            listeners.forEach { it.onRootViewsChanged(removedView, false) }
            return removedView
        }
    }
    
    companion object {
        fun install(): RootViewsSpy {
            return RootViewsSpy().apply {
                WindowManagerSpy.swapWindowManagerGlobalMViews { mViews /*原对象*/ ->
                    // 新对象(lambda 表达式的末行就是返回值)
                    delegatingViewList.apply { addAll(mViews) }
                }
            }
        }
    }
    

    WindowManageSpy.kt

    // Hook WMS 服务内部的 WindowManagerGlobal.mViews RootView 列表
    // swap 是一个 lambda 表达式,参数为原对象,返回值为注入的新对象
    fun swapWindowManagerGlobalMViews(swap: (ArrayList<View>) -> ArrayList<View>) {
        windowManagerInstance?.let { windowManagerInstance ->
            mViewsField?.let { mViewsField ->
                val mViews = mViewsField[windowManagerInstance] as ArrayList<View>
                mViewsField[windowManagerInstance] = swap(mViews)
            }
        }
    }
    

    至此,LeakCanary 初始化完成,并且成功在 Android Framework 的各个位置安插监控,实现对 Activity 和 Service 等对象进入无用状态的监听。我们可以用一张示意图描述 LeakCanary 的部分结构:

    6.3 LeakCanary 如何判定对象泄漏?

    在以上步骤中,当对象的使用生命周期结束后,会交给 ObjectWatcher 监控,现在我们来具体看下它是怎么判断对象发生泄漏的。主要逻辑概括为 3 步:

    • 第 1 步: 为被监控对象 watchedObject 创建一个 KeyedWeakReference 弱引用,并存储到 <UUID, KeyedWeakReference> 的映射表中;
    • 第 2 步: postDelay 五秒后检查引用对象是否出现在引用队列中,出现在队列则说明被监控对象未发生泄漏。随后,移除映射表中未泄露的记录,更新泄漏的引用对象的 retainedUptimeMillis 字段以标记为泄漏;
    • 第 3 步: 通过回调 onObjectRetained 告知 LeakCanary 内部发生新的内存泄漏。

    源码摘要如下:

    AppWatcher.kt

    val objectWatcher = ObjectWatcher(
        // lambda 表达式获取当前系统时间
        clock = { SystemClock.uptimeMillis() },
        // lambda 表达式实现 Executor SAM 接口
        checkRetainedExecutor = {
            mainHandler.postDelayed(it, retainedDelayMillis)
        },
        // lambda 表达式获取监控开关
        isEnabled = { true }
    )
    

    ObjectWatcher.kt

    class ObjectWatcher constructor(
        private val clock: Clock,
        private val checkRetainedExecutor: Executor,
        private val isEnabled: () -> Boolean = { true }
    ) : ReachabilityWatcher {
    
        if (!isEnabled()) {
            // 监控开关
            return
        }
    
        // 被监控的对象映射表 <UUID,KeyedWeakReference>
        private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
    
        // KeyedWeakReference 关联的引用队列,用于判断对象是否泄漏
        private val queue = ReferenceQueue<Any>()
    
        // 1. 为 watchedObject 对象增加监控
        @Synchronized 
        override fun expectWeaklyReachable(
            watchedObject: Any,
            description: String
        ) {
            // 1.1 移除 watchedObjects 中未泄漏的引用对象
            removeWeaklyReachableObjects()
            // 1.2 新建一个 KeyedWeakReference 引用对象
            val key = UUID.randomUUID().toString()
            val watchUptimeMillis = clock.uptimeMillis()
            watchedObjects[key] = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
            // 2. 五秒后检查引用对象是否出现在引用队列中,否则判定发生泄漏
            // checkRetainedExecutor 相当于 postDelay 五秒后执行 moveToRetained() 方法
            checkRetainedExecutor.execute {
                moveToRetained(key)
            }
        }
    
        // 2. 五秒后检查引用对象是否出现在引用队列中,否则说明发生泄漏
        @Synchronized 
        private fun moveToRetained(key: String) {
            // 2.1 移除 watchedObjects 中未泄漏的引用对象
            removeWeaklyReachableObjects()
            // 2.2 依然存在的引用对象被判定发生泄漏
            val retainedRef = watchedObjects[key]
            if (retainedRef != null) {
                retainedRef.retainedUptimeMillis = clock.uptimeMillis()
                // 3. 回调通知 LeakCanary 内部处理
                onObjectRetainedListeners.forEach { it.onObjectRetained() }
            }
        }
    
        // 移除未泄漏对象对应的 KeyedWeakReference
        private fun removeWeaklyReachableObjects() {
            var ref: KeyedWeakReference?
            do {
                ref = queue.poll() as KeyedWeakReference?
                if (ref != null) {
                    // KeyedWeakReference 出现在引用队列中,说明未发生泄漏
                    watchedObjects.remove(ref.key)
                }
            } while (ref != null)
        }
    
        // 4. Heap Dump 后移除所有监控时间早于 heapDumpUptimeMillis 的引用对象
        @Synchronized 
        fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {
            val weakRefsToRemove = watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }
            weakRefsToRemove.values.forEach { it.clear() }
            watchedObjects.keys.removeAll(weakRefsToRemove.keys)
        }
    
        // 获取是否有内存泄漏对象
        val hasRetainedObjects: Boolean
        @Synchronized get() {
            // 移除 watchedObjects 中未泄漏的引用对象
            removeWeaklyReachableObjects()
            return watchedObjects.any { it.value.retainedUptimeMillis != -1L }
        }
    
        // 获取内存泄漏对象计数
        val retainedObjectCount: Int
        @Synchronized get() {
            // 移除 watchedObjects 中未泄漏的引用对象
            removeWeaklyReachableObjects()
            return watchedObjects.count { it.value.retainedUptimeMillis != -1L }
        }
    }
    

    被监控对象 watchedObject 关联的弱引用对象:

    KeyedWeakReference.kt

    class KeyedWeakReference(
        // 被监控对象
        referent: Any,
        // 唯一 Key,根据此字段匹配映射表中的记录
        val key: String,
        // 描述信息
        val description: String,
        // 监控开始时间,即引用对象创建时间
        val watchUptimeMillis: Long,
        // 关联的引用队列
        referenceQueue: ReferenceQueue<Any>
    ) : WeakReference<Any>(referent, referenceQueue) {
      
        // 记录实际对象 referent 被判定为泄漏对象的时间
        // -1L 表示非泄漏对象,或者还未判定完成
        @Volatile
        var retainedUptimeMillis = -1L
    
        override fun clear() {
            super.clear()
            retainedUptimeMillis = -1L
        }
    
        companion object {
            // 记录最近一次触发 Heap Dump 的时间
            @Volatile
            @JvmStatic var heapDumpUptimeMillis = 0L
        }
    }
    

    6.4 LeakCanary 发现泄漏对象后就会触发分析吗?

    ObjectWatcher 判定被监控对象发生泄漏后,会通过接口方法 OnObjectRetainedListener#onObjectRetained() 回调到 LeakCanary 内部的管理器 InternalLeakCanary 处理(在前文 AppWatcher 初始化中提到过)。LeakCanary 不会每次发现内存泄漏对象都进行分析工作,而会进行两个拦截:

    • 拦截 1:泄漏对象计数未达到阈值,或者进入后台时间未达到阈值;
    • 拦截 2:计算距离上一次 HeapDump 未超过 60s。

    源码摘要如下:

    InternalLeakCanary.kt

    // 从 ObjectWatcher 回调过来
    override fun onObjectRetained() = scheduleRetainedObjectCheck()
    
    private lateinit var heapDumpTrigger: HeapDumpTrigger
    
    fun scheduleRetainedObjectCheck() {
        if (this::heapDumpTrigger.isInitialized) {
            heapDumpTrigger.scheduleRetainedObjectCheck()
        }
    }
    

    HeapDumpTrigger.kt

    fun scheduleRetainedObjectCheck(delayMillis: Long = 0L) {
        // 已简化:源码此处使用时间戳拦截,避免重复 postDelayed
        backgroundHandler.postDelayed({
            checkRetainedObjects()
        }, delayMillis)
    }
    
    private fun checkRetainedObjects() {
        val config = configProvider()
    
        // 泄漏对象计数
        var retainedReferenceCount = objectWatcher.retainedObjectCount
        if (retainedReferenceCount > 0) {
            // 主动触发 GC,并等待 100 ms
            gcTrigger.runGc()
            // 重新获取泄漏对象计数
            retainedReferenceCount = objectWatcher.retainedObjectCount
        }
    
        // 拦截 1:泄漏对象计数未达到阈值,或者进入后台时间未达到阈值
        if (retainedKeysCount < retainedVisibleThreshold) {
            // App 位于前台或者刚刚进入后台
            if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
                // 发送通知提醒
                showRetainedCountNotification("App visible, waiting until %d retained objects")
                // 延迟 2 秒再检查
                scheduleRetainedObjectCheck(WAIT_FOR_OBJECT_THRESHOLD_MILLIS)
                return;
            }
        }
    
        // 拦截 2:计算距离上一次 HeapDump 未超过 60s
        val now = SystemClock.uptimeMillis()
        val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
        if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
            // 发送通知提醒
            showRetainedCountNotification("Last heap dump was less than a minute ago")
            // 延迟 (60 - elapsedSinceLastDumpMillis)s 再检查
            scheduleRetainedObjectCheck(WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis)
            return
        }
        
        // 移除通知提醒
        dismissRetainedCountNotification()
        // 触发 HeapDump(此时,应用有可能在后台)
        dumpHeap(...)
    }
    
    // 真正开始执行 Heap Dump
    private fun dumpHeap(...) {
        // 1. 获取文件存储提供器
        val directoryProvider = InternalLeakCanary.createLeakDirectoryProvider(InternalLeakCanary.application)
    
        // 2. 创建 .hprof File 文件
        val heapDumpFile = directoryProvider.newHeapDumpFile()
    
        // 3. 执行 Heap Dump
        // Heap Dump 开始时间戳
        val heapDumpUptimeMillis = SystemClock.uptimeMillis()
        // heapDumper.dumpHeap:最终调用 Debug.dumpHprofData(heapDumpFile.absolutePath) 
        configProvider().heapDumper.dumpHeap(heapDumpFile)
    
        // 4. 清除 ObjectWatcher 中过期的监控
        objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
    
        // 5. 分析堆快照
        InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId!!, heapDumpFile, durationMillis, reason))
    }
    

    请求 GC 的源码可以看一眼:

    GcTrigger.kt

    fun interface GcTrigger {
    
        fun runGc()
    
        object Default : GcTrigger {
            override fun runGc() {
                // Runtime.gc() 相比于 System.gc() 更有可能触发 GC
                Runtime.getRuntime().gc()
                // 暂停等待 GC 
                Thread.sleep(100)
                System.runFinalization()
            }
        }
    }
    

    6.5 LeakCanary 在哪个线程分析堆快照?

    在前面的工作中,LeakCanary 已经成功生成 .hprof 堆快照文件,并且发送了一个 LeakCanary 内部事件 HeapDump。那么这个事件在哪里被消费的呢?

    一步步跟踪代码可以看到 LeakCanary 的配置项中设置了多个事件消费者 EventListener,其中与 HeapDump 事件有关的是 when{} 代码块中三个消费者。不过,这三个消费者并不是并存的,而是会根据 App 当前的依赖项而选择最优的执行策略:

    • 策略 1 - WorkerManager 多进程分析
    • 策略 2 - WorkManager 异步分析
    • 策略 3 - 异步线程分析(兜底策略)

    LeakCanary 配置项中的事件消费者:

    LeakCanary.kt

    data class Config(
        val eventListeners: List<EventListener> = listOf(
            LogcatEventListener,
            ToastEventListener,
            LazyForwardingEventListener {
                if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
            },
            when {
                // 策略 1 - WorkerManager 多进程分析
                RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->RemoteWorkManagerHeapAnalyzer
                // 策略 2 - WorkManager 异步分析
                WorkManagerHeapAnalyzer.validWorkManagerInClasspath -> WorkManagerHeapAnalyzer
                // 策略 3 - 异步线程分析(兜底策略)
                else -> BackgroundThreadHeapAnalyzer
            }
        ),
        ...
    )
    
    • 策略 1 - WorkerManager 多进程分析: 判断是否可以类加载 RemoteLeakCanaryWorkerService ,这个类位于前文提到的 com.squareup.leakcanary:leakcanary-android-process:2.9.1 依赖中。如果可以类加载成功则视为有依赖,使用 WorkerManager 多进程分析;

    RemoteWorkManagerHeapAnalyzer.kt

    object RemoteWorkManagerHeapAnalyzer : EventListener {
    
        // 通过类加载是否成功,判断是否存在依赖
        internal val remoteLeakCanaryServiceInClasspath by lazy {
            try {
                Class.forName("leakcanary.internal.RemoteLeakCanaryWorkerService")
                true
            } catch (ignored: Throwable) {
                false
            }
        }
    
        override fun onEvent(event: Event) {
            if (event is HeapDump) {
                // 创建并分发 WorkManager 多进程请求
                val heapAnalysisRequest = OneTimeWorkRequest.Builder(RemoteHeapAnalyzerWorker::class.java).apply {
                    val dataBuilder = Data.Builder()
                        .putString(ARGUMENT_PACKAGE_NAME, application.packageName)
                        .putString(ARGUMENT_CLASS_NAME, REMOTE_SERVICE_CLASS_NAME)
                    setInputData(event.asWorkerInputData(dataBuilder))
                    with(WorkManagerHeapAnalyzer) {
                        addExpeditedFlag()
                    }
                }.build()
                WorkManager.getInstance(application).enqueue(heapAnalysisRequest)
            }
        }
    }
    

    RemoteHeapAnalyzerWorker.kt

    internal class RemoteHeapAnalyzerWorker(appContext: Context, workerParams: WorkerParameters) : RemoteListenableWorker(appContext, workerParams) {
        override fun startRemoteWork(): ListenableFuture<Result> {
            val heapDump = inputData.asEvent<HeapDump>()
            val result = SettableFuture.create<Result>()
            heapAnalyzerThreadHandler.post {
                // 1.1 分析堆快照
                val doneEvent = AndroidDebugHeapAnalyzer.runAnalysisBlocking(heapDump, isCanceled = {
                    result.isCancelled
                }) { progressEvent ->
                    // 1.2 发送分析进度事件
                    if (!result.isCancelled) {
                        InternalLeakCanary.sendEvent(progressEvent)
                    }
                }
                // 1.3 发送分析完成事件
                InternalLeakCanary.sendEvent(doneEvent)
                result.set(Result.success())
            }
            return result
        }
    }
    
    • 策略 2 - WorkManager 异步分析: 判断是否可以类加载 androidx.work.WorkManager ,如果可以,则使用 WorkManager 异步分析;

    WorkManagerHeapAnalyzer.kt

    internal val validWorkManagerInClasspath by lazy {
        // 判断 WorkManager 依赖,代码略
    }
    
    override fun onEvent(event: Event) {
        if (event is HeapDump) {
            // 创建并分发 WorkManager 请求
            val heapAnalysisRequest = OneTimeWorkRequest.Builder(HeapAnalyzerWorker::class.java).apply {
                setInputData(event.asWorkerInputData())
                addExpeditedFlag()
            }.build()
            val application = InternalLeakCanary.application
            WorkManager.getInstance(application).enqueue(heapAnalysisRequest)
        }
    }
    

    HeapAnalyzerWorker.kt

    internal class HeapAnalyzerWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
        override fun doWork(): Result {
            // 2.1 分析堆快照
            val doneEvent = AndroidDebugHeapAnalyzer.runAnalysisBlocking(inputData.asEvent()) { event ->
                // 2.2 发送分析进度事件
                InternalLeakCanary.sendEvent(event)
            }
            // 2.3 发送分析完成事件
            InternalLeakCanary.sendEvent(doneEvent)
            return Result.success()
        }
    }
    
    • 策略 3 - 异步线程分析(兜底策略): 如果以上策略未命中,则直接使用子线程兜底执行。

    BackgroundThreadHeapAnalyzer.kt

    object BackgroundThreadHeapAnalyzer : EventListener {
    
        // HandlerThread
        internal val heapAnalyzerThreadHandler by lazy {
            val handlerThread = HandlerThread("HeapAnalyzer")
            handlerThread.start()
            Handler(handlerThread.looper)
        }
    
        override fun onEvent(event: Event) {
            if (event is HeapDump) {
                // HandlerThread 请求
                heapAnalyzerThreadHandler.post {
                    // 3.1 分析堆快照
                    val doneEvent = AndroidDebugHeapAnalyzer.runAnalysisBlocking(event) { event ->
                        // 3.2 发送分析进度事件
                        InternalLeakCanary.sendEvent(event)
                    }
                    // 3.3 发送分析完成事件
                    InternalLeakCanary.sendEvent(doneEvent)
                }
            }
        }
    }
    

    可以看到,不管采用那种执行策略,最终执行的逻辑都是一样的:

    • 1、分析堆快照;
    • 2、发送分析进度事件;
    • 3、发送分析完成事件。

    6.5 LeakCanary 如何分析堆快照?

    在前面的分析中,我们已经知道 LeakCanary 是通过子线程或者子进程执行 AndroidDebugHeapAnalyzer.runAnalysisBlocking 方法来分析堆快照的,并在分析过程中和分析完成后发送回调事件。现在我们来阅读 LeakCanary 的堆快照分析过程:

    AndroidDebugHeapAnalyzer.kt

    fun runAnalysisBlocking(
        heapDumped: HeapDump,
        isCanceled: () -> Boolean = { false },
        progressEventListener: (HeapAnalysisProgress) -> Unit
    ): HeapAnalysisDone<*> {
        ...
        // 1. .hprof 文件
        val heapDumpFile = heapDumped.file
        // 2. 分析堆快照
        val heapAnalysis = analyzeHeap(heapDumpFile, progressListener, isCanceled)
        val analysisDoneEvent = ScopedLeaksDb.writableDatabase(application) { db ->
        // 3. 将分析报告持久化到 DB
        val id = HeapAnalysisTable.insert(db, heapAnalysis)
        // 4. 发送分析完成事件(返回到上一级进行发送:InternalLeakCanary.sendEvent(doneEvent))
        val showIntent = LeakActivity.createSuccessIntent(application, id)
        val leakSignatures = fullHeapAnalysis.allLeaks.map { it.signature }.toSet()
        val leakSignatureStatuses = LeakTable.retrieveLeakReadStatuses(db, leakSignatures)
        val unreadLeakSignatures = leakSignatureStatuses.filter { (_, read) -> !read}.keys.toSet()
            HeapAnalysisSucceeded(heapDumped.uniqueId, fullHeapAnalysis, unreadLeakSignatures ,showIntent)
        }
        return analysisDoneEvent
    }
    

    核心分析方法是 analyzeHeap(…),继续往下走:

    AndroidDebugHeapAnalyzer.kt

    private fun analyzeHeap(
        heapDumpFile: File,
        progressListener: OnAnalysisProgressListener,
        isCanceled: () -> Boolean
    ): HeapAnalysis {
        ...
        // Shark 堆快照分析器
        val heapAnalyzer = HeapAnalyzer(progressListener)
        ...
        // 构建对象图信息
        val sourceProvider = ConstantMemoryMetricsDualSourceProvider(ThrowingCancelableFileSourceProvider(heapDumpFile)
        val graph = sourceProvider.openHeapGraph(proguardMapping = proguardMappingReader?.readProguardMapping())
        ...
        // 开始分析
        heapAnalyzer.analyze(
        heapDumpFile = heapDumpFile,
        graph = graph,
        leakingObjectFinder = config.leakingObjectFinder, // 默认是 KeyedWeakReferenceFinder
        referenceMatchers = config.referenceMatchers, // 默认是 AndroidReferenceMatchers
        computeRetainedHeapSize = config.computeRetainedHeapSize, // 默认是 true
        objectInspectors = config.objectInspectors, // 默认是 AndroidObjectInspectors
        metadataExtractor = config.metadataExtractor // 默认是 AndroidMetadataExtractor
        )
    }
    

    开始进入 Shark 组件:

    shark.HeapAnalyzer.kt

    // analyze -> analyze -> FindLeakInput.analyzeGraph
    private fun FindLeakInput.analyzeGraph(
        metadataExtractor: MetadataExtractor,
        leakingObjectFinder: LeakingObjectFinder,
        heapDumpFile: File,
        analysisStartNanoTime: Long
    ): HeapAnalysisSuccess {
        ...
        // 1. 在堆快照中寻找泄漏对象,默认是寻找 KeyedWeakReference 类型对象
        // leakingObjectFinder 默认是 KeyedWeakReferenceFinder
        val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
        // 2. 分析泄漏对象的最短引用链,并按照应用链签名分类
        // applicationLeaks: Application Leaks
        // librbuildLeakTracesaryLeaks:Library Leaks
        // unreachableObjects:LeakCanary 无法分析出强引用链,可以提 Stack Overflow
        val (applicationLeaks, libraryLeaks, unreachableObjects) = findLeaks(leakingObjectIds)
        // 3. 返回分析完成事件
        return HeapAnalysisSuccess(...)
    }
    
    private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): LeaksAndUnreachableObjects {
        // PathFinder:引用链分析器
        val pathFinder = PathFinder(graph, listener, referenceReader, referenceMatchers)
        // pathFindingResults:完整引用链
        val pathFindingResults = pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)
        // unreachableObjects:LeakCanary 无法分析出强引用链(相当于 LeakCanary 的 Bug)
        val unreachableObjects = findUnreachableObjects(pathFindingResults, leakingObjectIds)
        // shortestPaths:最短引用链
        val shortestPaths = deduplicateShortestPaths(pathFindingResults.pathsToLeakingObjects)
        // inspectedObjectsByPath:标记信息
        val inspectedObjectsByPath = inspectObjects(shortestPaths)
        // retainedSizes:泄漏内存大小
        val retainedSizes = computeRetainedSizes(inspectedObjectsByPath, pathFindingResults.dominatorTree)
        // 生成单个泄漏问题的分析报告,并按照应用链签名分组,按照 Application Leaks 和 Library Leaks 分类,按照 Application Leaks 和 Library Leaks 分类
        // applicationLeaks: Application Leaks
        // librbuildLeakTracesaryLeaks:Library Leaks
        val (applicationLeaks, librbuildLeakTracesaryLeaks) = buildLeakTraces(shortestPaths, inspectedObjectsByPath, retainedSizes)
        return LeaksAndUnreachableObjects(applicationLeaks, libraryLeaks, unreachableObjects)
    }
    

    可以看到,堆快照分析最终是交给 Shark 中的 HeapAnalizer 完成的,核心流程是:

    • 1、在堆快照中寻找泄漏对象,默认是寻找 KeyedWeakReference 类型对象;
    • 2、分析 KeyedWeakReference 对象的最短引用链,并按照引用链签名分组,按照 Application Leaks 和 Library Leaks 分类;
    • 3、返回分析完成事件。

    第 1 步和第 3 步不用说了,继续分析最复杂的第 2 步:

    shark.HeapAnalyzer.kt

    // 生成单个泄漏问题的分析报告,并按照应用链签名分组,按照 Application Leaks 和 Library Leaks 分类,按照 Application Leaks 和 Library Leaks 分类
    private fun FindLeakInput.buildLeakTraces(
        shortestPaths: List<ShortestPath> /*最短引用链*/ ,
        inspectedObjectsByPath: List<List<InspectedObject>> /*标记信息*/ ,
        retainedSizes: Map<Long, Pair<Int, Int>>? /*泄漏内存大小*/
    ): Pair<List<ApplicationLeak>, List<LibraryLeak>> {
        // Application Leaks
        val applicationLeaksMap = mutableMapOf<String, MutableList<LeakTrace>>()
        // Library Leaks
        val libraryLeaksMap = mutableMapOf<String, Pair<LibraryLeakReferenceMatcher, MutableList<LeakTrace>>>()
    
        shortestPaths.forEachIndexed { pathIndex, shortestPath ->
            // 标记信息
            val inspectedObjects = inspectedObjectsByPath[pathIndex]
            // 实例化引用链上的每个对象快照(非怀疑对象的 leakingStatus 为 NOT_LEAKING)
            val leakTraceObjects = buildLeakTraceObjects(inspectedObjects, retainedSizes)
            val referencePath = buildReferencePath(shortestPath, leakTraceObjects)
            // 分析报告
            val leakTrace = LeakTrace(
                gcRootType = GcRootType.fromGcRoot(shortestPath.root.gcRoot),
                referencePath = referencePath,
                leakingObject = leakTraceObjects.last()
            )
            val firstLibraryLeakMatcher = shortestPath.firstLibraryLeakMatcher()
            if (firstLibraryLeakMatcher != null) {
                // Library Leaks
                val signature: String = firstLibraryLeakMatcher.pattern.toString().createSHA1Hash()
                libraryLeaksMap.getOrPut(signature) { firstLibraryLeakMatcher to mutableListOf() }.second += leakTrace
            } else {
                // Application Leaks
                applicationLeaksMap.getOrPut(leakTrace.signature) { mutableListOf() } += leakTrace
            }
        }
        val applicationLeaks = applicationLeaksMap.map { (_, leakTraces) ->
            // 实例化为 ApplicationLeak 类型
            ApplicationLeak(leakTraces)
        }
        val libraryLeaks = libraryLeaksMap.map { (_, pair) ->
            // 实例化为 LibraryLeak 类型
            val (matcher, leakTraces) = pair
            LibraryLeak(leakTraces, matcher.pattern, matcher.description)
        }
        return applicationLeaks to libraryLeaks
    }
    

    6.6 LeakCanary 如何筛选 ~~~ 怀疑对象?

    LeakCanary 会使用 ObjectInspector 对象检索器在引用链上的节点中标记必要的信息和状态,标记信息会显示在分析报告中,并且会影响报告中的提示。而引用链 LEAKING 节点以后到第一个 NOT_LEAKING 节点中间的节点,才会用 ~~~ 下划线标记为怀疑对象。

    在第 6.5 节中,LeakCanary 通过 leakingObjectFinder 标记引用信息,leakingObjectFinder 默认是 AndroidObjectInspectors.appDefaults ,也可以在配置项中自定义。

    // inspectedObjectsByPath:筛选出非怀疑对象(分析报告中 ~~~ 标记的是怀疑对象)
    val inspectedObjectsByPath = inspectObjects(shortestPaths)
    

    看一下可视化报告中相关源码:

    DisplayLeakAdapter.kt

    ...
    val reachabilityString = when (leakingStatus) {
        UNKNOWN -> extra("UNKNOWN")
        NOT_LEAKING -> "NO" + extra(" (${leakingStatusReason})")
        LEAKING -> "YES" + extra(" (${leakingStatusReason})")
    }
    ...
    

    LeakTrace.kt

    // 是否为怀疑对象
    fun referencePathElementIsSuspect(index: Int): Boolean {
        return  when (referencePath[index].originObject.leakingStatus) {
            UNKNOWN -> true
            NOT_LEAKING -> index == referencePath.lastIndex || referencePath[index + 1].originObject.leakingStatus != NOT_LEAKING
            else -> false
        }
    }
    

    6.7 LeakCanary 分析完成后的处理

    有两个位置处理了 HeapAnalysisSucceeded 事件:

    • Logcat:打印分析报告日志;
    • Notification: 发送分析成功系统通知消息。

    LogcatEventListener.kt

    object LogcatEventListener : EventListener {
        ...
        SharkLog.d { "\u200B\n${LeakTraceWrapper.wrap(event.heapAnalysis.toString(), 120)}" }
        ...
    }
    

    NotificationEventListener.kt

    object NotificationEventListener : EventListener {
        ...
        val flags = if (Build.VERSION.SDK_INT >= 23) {
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        } else {
            PendingIntent.FLAG_UPDATE_CURRENT
        }
        // 点击通知消息打开可视化分析报告
        val pendingIntent = PendingIntent.getActivity(appContext, 1,  event.showIntent, flags)
        showHeapAnalysisResultNotification(contentTitle,pendingIntent)
        ...
    }
    

    至此,LeakCanary 原理分析完毕。


    7. 总结

    到这里,LeakCanary 的使用和原理分析就讲完了。不过,LeakCanary 毕竟是实验室使用的工具,如果要实现线上内存泄漏监控,你知道怎么做吗?要实现 Native 内存泄漏监控又要怎么做?关注我,带你了解更多。


    参考资料

    相关文章

      网友评论

        本文标题:为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary

        本文链接:https://www.haomeiwen.com/subject/gucggrtx.html