⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
当一个开发者或团队的水平积累到一定程度,会有自内向外输出价值的需求。在这个专栏里,小彭将为你分享 Android 方向主流开源组件的实现原理,包括网络、存储、UI、监控等。
本文是 Android 开源库系列的第 7 篇文章,完整文章目录请移步到文章末尾~
前言
LeakCanary 是我们非常熟悉内存泄漏检测工具,它能够帮助开发者非常高效便捷地检测 Android 中常见的内存泄漏。在各大厂自研的内存泄漏检测框架(如腾讯 Matrix 和快手 Koom)的帮助文档中,也会引述 LeakCanary 原理分析。
不吹不黑,LeakCanary 源码中除了实现内存泄漏的监控方案外,还有非常多值得学习的编程技巧,只有沉下心去阅读的人才能够真正体会到。在这篇文章里,我将带你从入门开始掌握 LeakCanary 的使用场景以及使用方法,再介绍 LeakCanary 的工作流程和高级用法,最后通过源码解析深入理解原理。本文示例程序已上传到 Github: DemoHall · HelloLeakCanary ,有用请给 Star 支持,谢谢。
提示: 本文源码分析基于 2022 年 4 月发布的 LeakCanary 2.9.1。
本文原理分析涉及的 Java 虚拟机内存管理基础:
本文源码分析涉及的 Android 原理基础:
- Android Jetpack 开发套件 #3 为什么 Activity 都重建了 ViewModel 还存在?
- Android Jetpack 开发套件 #7 AndroidX Fragment 核心原理分析
- Android Jetpack 开发套件 #9 食之无味!App Startup 可能比你想象中要简单
- 4、Framework · ContentProvider 启动过程分析
- 5、Framework · Activity 启动过程分析
- 6、Framework · Service 启动过程分析
学习路线图:
- 认识 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、利用引用对象可感知对象垃圾回收的机制判定内存泄漏: 为无用对象包装弱引用,并在一段时间后(默认为五秒)观察弱引用是否如期进入关联的引用队列,是则说明未发生泄漏,否则说明发生泄漏(无用对象被强引用持有,导致无法回收,即泄漏)。
详细的源码分析下文内容。
- 理解 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 一次内存泄漏分析工作流程执行完毕。
- LeakCanary 的基本用法
这一节,我们来介绍 LeakCanary 的基础用法。
3.1 将 LeakCanary 添加到项目中
在 build.gradle 中添加 LeakCanary 依赖,此外不需要调用任何初始化 API(LeakCanary 内部默认使用了 ContentProvider 实现无侵入初始化)。另外,因为 LeakCanary 是只在实验室环境使用的工具,所以这里要记得使用 debugImplementation
依赖配置。
build.gradle
1 | groovy复制代码dependencies { |
3.2 手动初始化 LeakCanary
LeakCanary 2.0 默认采用了 ContentProvider 机制实现了无侵入初始化,为了给予开发者手动初始化 LeakCanary 的可能性,LeakCanary 在 ContentProvider 中设置了布尔值开关:
AndroidManifest.xml
1 | xml复制代码<application> |
开发者只需要在资源文件里覆写 @bool/eak_canary_watcher_auto_install
布尔值来关闭自动初始化,并在合适的时机手动调用 AppWatcher#manualInstall
。
values.xml
1 | xml复制代码<resources> |
3.3 自定义 LeakCanary 配置
LeakCanary 为开发者提供了便捷的配置 API,并且这个配置 API 在初始化前后都允许调用。
示例程序
1 | java复制代码// Java 语法 |
1 | kotlin复制代码// Kotlin 语法 |
以下用一个表格总结 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 | 事件监听器 | 多个内部监听器 |
- 解读 LeakCanary 分析报告
内存泄漏分析报告是 LeakCanary 所有监控和分析工作后输出的目标产物,要根据修复内存泄漏,首先就要求开发者能够读懂 LeakCanary 的分析报告。我将 LeakCanary 的分析报告总结为以下 4 个要点:
4.1 泄漏对象的引用链
泄漏对象的引用链是分析报告的核心信息,LeakCanary 会收集泄漏对象到 GC Root 的完整引用链信息。例如,以下示例程序在 static 变量中持有一个 Helper
对象,当 Helper 被期望被垃圾回收时用 AppWatcher 监测该对象,如果未按预期被回收,则会输出以下分析报告:
示例程序
1 | java复制代码class Helper { |
Logcat 日志
1 | bash复制代码┬─── |
解释一下其中的符号:
├
代表一个 Java 对象;│ ↓
代表一个 Java 引用,关联的实际对象在下一行;╰→
代表泄漏的对象,即AppWatcher.objectWatcher.watch()
直接监控的对象。
4.2 按引用链签名分组
用减少重复的排查工作,LeakCanary 会将相同问题重复触发的内存泄漏进行分组,分组方法是按引用链的签名。引用链签名是对引用链上经过的每个对象的类型拼接后取哈希值,既然应用链完全相同,就没必要重复排查了。
例如,对于泄漏对象 instance
,对应的泄漏签名计算公式如下:
Logcat 日志
1 | bash复制代码... |
对应的签名计算公式
1 | bash复制代码val leakSignature = sha1Hash( |
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 标记
1 | bash复制代码==================================== |
可视化分析报告中的 Library Leak 标记
- LeakCanary 的进阶用法
5.1 使用 App Startup 初始化 LeakCanary
LeakCanary 2.8 提供了对 Jetpack · App Startup 的支持。如果想使用 App Startup 初始化 LeakCanary,只需要替换为另一个依赖。不过,毕竟 LeakCanary 是主要在实验室环境使用的工具,这个优化的意义并不大。
build.gradle
1 | groovy复制代码dependencies { |
对应的 App Startup 启动器源码:
AppWatcherStartupInitializer.kt
1 | kotlin复制代码internal class AppWatcherStartupInitializer : Initializer<AppWatcherStartupInitializer> { |
5.2 在子进程执行 LeakCanary 分析工作
由于 LeakCanary 分析堆快照的过程存在一定的内存消耗,整个分析过程一般会持续几十秒,对于一些性能差的机型会造成明显的卡顿甚至 ANR。为了优化内存占用和卡顿问题,LeakCanary 2.8 提供了对多进程的支持。开发者只需要依赖 LeakCanary 的多进程依赖项,LeakCanary 会自动将分析工作转移到子进程中(基于 androidX.work.multiprocess
):
build.gradle
1 | groovy复制代码dependencies { |
同时,开发者需要在自定义 Application 中检查当前进程信息,避免在 LeakCanary 的子进程中执行不必要的初始化操作:
ExampleApplication.kt
1 | kotlin复制代码class ExampleApplication : Application() { |
Logcat 进程选项
Logcat 日志
1 | java复制代码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 执行器,示例程序如下:
示例程序
1 | kotlin复制代码// 依赖: |
Logcat 日志对比
1 | bash复制代码// 使用默认的 Debug.dumpHprofData() 的日志 |
看一眼 Koom 源码:
ForkJvmHeapDumper.java
1 | java复制代码public synchronized boolean dump(String path) { |
5.4 自定义标记引用信息
LeakCanary 配置项可以自定义 ObjectInspector 对象检索器,在引用链上的节点中标记必要的信息和状态。标记信息会显示在分析报告中,并且会影响报告中的提示。
- notLeakingReasons 标记: 标记非泄漏原因后,节点为 NOT_LEAKING 状态,并在分析报告中会显示
Leaking: NO (notLeakingReasons)
; - leakingReasons 标记: 标记泄漏原因后,节点为 LEAKING 状态,在分析报告中会显示
Leaking: YES (leakingReasons)
; - 缺省: 节点为 UNKNOWN 状态,在分析报告中会显示
Leaking: UNKNOWN
。
示例程序如下:
示例程序
1 | kotlin复制代码// 自定义 LeakCanary 配置 |
另外,引用链 LEAKING 节点以后到第一个 NOT_LEAKING 节点中间的节点,才会用 ~~~
下划线标记为怀疑对象。例如:
- LeakCanary 实现原理分析
使用一张示意图表示 LeakCanary 的基本架构:
6.1 LeakCanary 如何实现自动初始化?
旧版本的 LeakCanary 需要在 Application 中调用相关初始化 API,而在 LeakCanary v2 版本中却不再需要手动初始化,为什么呢?—— 这是因为 LeakCanary 利用了 ContentProvider 的初始化机制来间接调用初始化 API。
ContentProvider 的常规用法是提供内容服务,而另一个特殊的用法是提供无侵入的初始化机制,这在第三方库中很常见,Jetpack 中提供的轻量级初始化框架 App Startup 也是基于 ContentProvider 的方案。
MainProcessAppWatcherInstaller.kt
1 | kotlin复制代码internal class MainProcessAppWatcherInstaller : ContentProvider() { |
6.2 LeakCanary 初始化过程分析
LeakCanary 的初始化工程可以概括为 2 项内容:
- 1、初始化 LeakCanary 内部分析引擎;
- 2、在 Android Framework 上注册五种 Android 泄漏场景的监控。
AppWathcer.kt
1 | kotlin复制代码// LeakCanary 初始化 API |
下面展开具体分析:
初始化内容 1 - 初始化 LeakCanary 内部分析引擎: 创建 HeapDumpTrigger 触发器,并在 Android Framework 上注册前后台切换监听、前台 Activity 监听和 ObjectWatcher 的泄漏监听。
InternalLeakCanary.kt
1 | kotlin复制代码override fun invoke(application: Application) { |
HeapDumpTrigger.kt
1 | kotlin复制代码// App 前后台切换状态变化回调 |
初始化内容 2 - 在 Android Framework 中注入对五种 Android 泄漏场景的监控: 实现在对象的使用生命周期结束后,自动将对象交给 ObjectWatcher
进行监控。
以下为 5 种 Android 泄漏场景的监控原理分析:
- 1、Activity 监控: 通过
Application#registerActivityLifecycleCallbacks(…)
接口监听 Activity#onDestroy 事件,将当前 Activity 对象交给 ObjectWatcher 监控;
ActivityWatcher.kt
1 | kotlin复制代码private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks by noOpDelegate() { |
- 2、Fragment 与 Fragment View 监控: 通过
FragmentAndViewModelWatcher
实现,首先是通过Application#registerActivityLifecycleCallbacks(…)
接口监听 Activity#onCreate 事件,再通过FragmentManager#registerFragmentLifecycleCallbacks(…)
接口监听 Fragment 的生命周期:
FragmentAndViewModelWatcher.kt
1 | kotlin复制代码// fragmentDestroyWatchers 是一个 Lambda 表达式数组 |
以 AndroidX Fragment 为例:
AndroidXFragmentDestroyWatcher.kt
1 | kotlin复制代码override fun invoke(activity: Activity) { |
- 3、ViewModel 监控: 由于 Android Framework 未提供设置 ViewModel#onClear() 全局监听的方法,所以 LeakCanary 是通过 Hook 的方式实现。即:在 Activity#onCreate 和 Fragment#onCreate 事件中实例化一个自定义ViewModel,在进入 ViewModel#onClear() 方法时,通过反射获取当前作用域中所有的 ViewModel 对象交给 ObjectWatcher 监控。
ViewModelClearedWatcher.kt
1 | kotlin复制代码// ViewModel 的子类 |
- 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
1 | kotlin复制代码private var uninstallActivityThreadHandlerCallback: (() -> Unit)? = null |
- 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
1 | kotlin复制代码override fun install() { |
curtains 源码摘要如下:
RootViewsSpy.kt
1 | kotlin复制代码private val delegatingViewList = object : ArrayList<View>() { |
WindowManageSpy.kt
1 | kotlin复制代码// Hook WMS 服务内部的 WindowManagerGlobal.mViews RootView 列表 |
至此,LeakCanary 初始化完成,并且成功在 Android Framework 的各个位置安插监控,实现对 Activity 和 Service 等对象进入无用状态的监听。我们可以用一张示意图描述 LeakCanary 的部分结构:
6.3 LeakCanary 如何判定对象泄漏?
在以上步骤中,当对象的使用生命周期结束后,会交给 ObjectWatcher
监控,现在我们来具体看下它是怎么判断对象发生泄漏的。主要逻辑概括为 3 步:
- 第 1 步: 为被监控对象
watchedObject
创建一个KeyedWeakReference
弱引用,并存储到 <UUID, KeyedWeakReference> 的映射表中; - 第 2 步: postDelay 五秒后检查引用对象是否出现在引用队列中,出现在队列则说明被监控对象未发生泄漏。随后,移除映射表中未泄露的记录,更新泄漏的引用对象的
retainedUptimeMillis
字段以标记为泄漏; - 第 3 步: 通过回调
onObjectRetained
告知 LeakCanary 内部发生新的内存泄漏。
源码摘要如下:
AppWatcher.kt
1 | kotlin复制代码val objectWatcher = ObjectWatcher( |
ObjectWatcher.kt
1 | kotlin复制代码class ObjectWatcher constructor( |
被监控对象 watchedObject
关联的弱引用对象:
KeyedWeakReference.kt
1 | kotlin复制代码class KeyedWeakReference( |
6.4 LeakCanary 发现泄漏对象后就会触发分析吗?
ObjectWatcher 判定被监控对象发生泄漏后,会通过接口方法 OnObjectRetainedListener#onObjectRetained()
回调到 LeakCanary 内部的管理器 InternalLeakCanary 处理(在前文 AppWatcher 初始化中提到过)。LeakCanary 不会每次发现内存泄漏对象都进行分析工作,而会进行两个拦截:
- 拦截 1:泄漏对象计数未达到阈值,或者进入后台时间未达到阈值;
- 拦截 2:计算距离上一次 HeapDump 未超过 60s。
源码摘要如下:
InternalLeakCanary.kt
1 | kotlin复制代码// 从 ObjectWatcher 回调过来 |
HeapDumpTrigger.kt
1 | kotlin复制代码fun scheduleRetainedObjectCheck(delayMillis: Long = 0L) { |
请求 GC 的源码可以看一眼:
GcTrigger.kt
1 | kotlin复制代码fun interface GcTrigger { |
6.5 LeakCanary 在哪个线程分析堆快照?
在前面的工作中,LeakCanary 已经成功生成 .hprof
堆快照文件,并且发送了一个 LeakCanary 内部事件 HeapDump
。那么这个事件在哪里被消费的呢?
一步步跟踪代码可以看到 LeakCanary 的配置项中设置了多个事件消费者 EventListener,其中与 HeapDump 事件有关的是 when{}
代码块中三个消费者。不过,这三个消费者并不是并存的,而是会根据 App 当前的依赖项而选择最优的执行策略:
- 策略 1 - WorkerManager 多进程分析
- 策略 2 - WorkManager 异步分析
- 策略 3 - 异步线程分析(兜底策略)
LeakCanary 配置项中的事件消费者:
LeakCanary.kt
1 | kotlin复制代码data class Config( |
- 策略 1 - WorkerManager 多进程分析: 判断是否可以类加载
RemoteLeakCanaryWorkerService
,这个类位于前文提到的com.squareup.leakcanary:leakcanary-android-process:2.9.1
依赖中。如果可以类加载成功则视为有依赖,使用 WorkerManager 多进程分析;
RemoteWorkManagerHeapAnalyzer.kt
1 | kotlin复制代码object RemoteWorkManagerHeapAnalyzer : EventListener { |
RemoteHeapAnalyzerWorker.kt
1 | kotlin复制代码internal class RemoteHeapAnalyzerWorker(appContext: Context, workerParams: WorkerParameters) : RemoteListenableWorker(appContext, workerParams) { |
- 策略 2 - WorkManager 异步分析: 判断是否可以类加载
androidx.work.WorkManager
,如果可以,则使用 WorkManager 异步分析;
WorkManagerHeapAnalyzer.kt
1 | kotlin复制代码internal val validWorkManagerInClasspath by lazy { |
HeapAnalyzerWorker.kt
1 | kotlin复制代码internal class HeapAnalyzerWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { |
- 策略 3 - 异步线程分析(兜底策略): 如果以上策略未命中,则直接使用子线程兜底执行。
BackgroundThreadHeapAnalyzer.kt
1 | kotlin复制代码object BackgroundThreadHeapAnalyzer : EventListener { |
可以看到,不管采用那种执行策略,最终执行的逻辑都是一样的:
- 1、分析堆快照;
- 2、发送分析进度事件;
- 3、发送分析完成事件。
6.5 LeakCanary 如何分析堆快照?
在前面的分析中,我们已经知道 LeakCanary 是通过子线程或者子进程执行 AndroidDebugHeapAnalyzer.runAnalysisBlocking
方法来分析堆快照的,并在分析过程中和分析完成后发送回调事件。现在我们来阅读 LeakCanary 的堆快照分析过程:
AndroidDebugHeapAnalyzer.kt
1 | kotlin复制代码fun runAnalysisBlocking( |
核心分析方法是 analyzeHeap(…)
,继续往下走:
AndroidDebugHeapAnalyzer.kt
1 | kotlin复制代码private fun analyzeHeap( |
开始进入 Shark 组件:
shark.HeapAnalyzer.kt
1 | kotlin复制代码// analyze -> analyze -> FindLeakInput.analyzeGraph |
可以看到,堆快照分析最终是交给 Shark 中的 HeapAnalizer 完成的,核心流程是:
- 1、在堆快照中寻找泄漏对象,默认是寻找 KeyedWeakReference 类型对象;
- 2、分析 KeyedWeakReference 对象的最短引用链,并按照引用链签名分组,按照 Application Leaks 和 Library Leaks 分类;
- 3、返回分析完成事件。
第 1 步和第 3 步不用说了,继续分析最复杂的第 2 步:
shark.HeapAnalyzer.kt
1 | kotlin复制代码// 生成单个泄漏问题的分析报告,并按照应用链签名分组,按照 Application Leaks 和 Library Leaks 分类,按照 Application Leaks 和 Library Leaks 分类 |
6.6 LeakCanary 如何筛选 ~ 怀疑对象?
LeakCanary 会使用 ObjectInspector 对象检索器在引用链上的节点中标记必要的信息和状态,标记信息会显示在分析报告中,并且会影响报告中的提示。而引用链 LEAKING
节点以后到第一个 NOT_LEAKING
节点中间的节点,才会用 ~~~
下划线标记为怀疑对象。
在第 6.5 节中,LeakCanary 通过 leakingObjectFinder
标记引用信息,leakingObjectFinder 默认是 AndroidObjectInspectors.appDefaults
,也可以在配置项中自定义。
1 | kotlin复制代码// inspectedObjectsByPath:筛选出非怀疑对象(分析报告中 ~~~ 标记的是怀疑对象) |
看一下可视化报告中相关源码:
DisplayLeakAdapter.kt
1 | kotlin复制代码... |
LeakTrace.kt
1 | kotlin复制代码// 是否为怀疑对象 |
6.7 LeakCanary 分析完成后的处理
有两个位置处理了 HeapAnalysisSucceeded
事件:
- Logcat:打印分析报告日志;
- Notification: 发送分析成功系统通知消息。
LogcatEventListener.kt
1 | kotlin复制代码object LogcatEventListener : EventListener { |
NotificationEventListener.kt
1 | kotlin复制代码object NotificationEventListener : EventListener { |
至此,LeakCanary 原理分析完毕。
- 总结
到这里,LeakCanary 的使用和原理分析就讲完了。不过,LeakCanary 毕竟是实验室使用的工具,如果要实现线上内存泄漏监控,你知道怎么做吗?要实现 Native 内存泄漏监控又要怎么做?关注我,带你了解更多。
参考资料
- LeakCanary 官网
- LeakCanary Github 仓库
- How Leakcanary leverages WorkManager multi-process —— Pierre-Yves Ricau 著
- Matrix Android ResourceCanary —— 腾讯 Matrix 说明文档
- KOOM —— 高性能线上内存监控方案 —— 快手 Koom 说明文档
- 内存优化(下):内存优化这件事,应该从哪里着手? —— 张绍文 著
- Android内存泄露检测 LeakCanary 2.0 (Kotlin版) 的实现原理 —— vivo 技术团队 著
推荐阅读
Android 开源库系列完整目录如下(2023/07/12 更新):
- #1 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(上)
- #2 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)
- #3 IO 框架 Okio 的实现原理,到底哪里 OK?
- #4 IO 框架 Okio 的实现原理,如何检测超时?
- #5 序列化框架 Gson 原理分析,可以优化吗?
- #6 适可而止!看 Glide 如何把生命周期安排得明明白白
- #7 为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为它是真强啊!
- #8 内存缓存框架 LruCache 的实现原理,手写试试?
- #9 这是一份详细的 EventBus 使用教程
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~
本文转载自: 掘金