继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为它是真强啊!

彭旭锐
关注TA
已关注
手记 118
粉丝 18
获赞 36

请点赞关注,你的支持对我意义重大。

前言

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

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

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


学习路线图:


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 会发送一条系统通知,你也可以点击该通知提前触发分析工作;

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

提示: 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 对象检索器 AndroidObjectInspectors.appDefaults
computeRetainedHeapSize: Boolean 是否计算泄漏内存空间 true
maxStoredHeapDumps: Int 最大堆快照存储数量 7
requestWriteExternalStoragePermission: Boolean 是否请求文件存储权限 true
leakingObjectFinder: LeakingObjectFinder 引用链分析器 KeyedWeakReferenceFinder
heapDumper: HeapDumper Heap Dump 执行器 Debug.dumpHprofData
eventListeners: List 事件监听器 多个内部监听器

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

可视化分析报告中的 Library Leak 标记


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 内存泄漏监控又要怎么做?关注我,带你了解更多。


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP