LeakCanary内存收集
# 01.LeakCanary内存泄漏检测原理
# 目录介绍
- 01.整体概述介绍
- 1.1 项目背景介绍
- 1.2 内存泄漏概念
- 1.3 设计目标
- 1.4 产生收益
- 02.开发设计思路
- 2.1 整体设计思路
- 2.2 初始化思路
- 2.3 设计组件监听
- 2.4 无用对象监听
- 2.5 内存泄漏监听
- 2.6 Dump内存
- 2.7 分析内存快照
- 2.8 输出堆栈链报告
- 2.9 KOOM设计思路
- 03.LeakCanary原理
- 3.1 要思考一些问题
- 3.2 原理流程的概括
- 3.3 初始化流程
- 3.4 监听组件销毁
- 3.5 监听无用对象
- 3.6 监控内存泄漏
- 3.7 Dump内存快照
- 3.8 分析堆快照
- 3.9 输出分析报告
- 04.一些技术点思考
- 4.1 WeakReference与ReferenceQueue机制
- 4.2 引用链如何生成
- 4.3 Shark分析引擎原理
- 4.4 提高Dump分析效率
- 4.5 相同问题分组
- 4.6 如何标记怀疑对象
- 05.优秀代码设计解析
- 5.1 设计模式在LeakCanary中的应用
- 5.2 sealed class的巧妙使用
- 5.3 委托模式与扩展函数的运用
- 5.4 KeyedWeakReference的设计
- 5.5 Clock抽象与可测试性设计
- 5.6 Shark引擎中的图遍历算法设计
- 06.方案基础设计
- 6.1 整体架构图
- 6.2 UML设计图
- 6.3 关键流程图
- 6.4 接口设计图
- 6.5 模块间依赖关系
- 07.其他设计说明
- 6.1 性能设计优化
- 6.2 稳定性设计
- 6.3 灰度设计
- 6.4 降级设计
- 6.5 异常设计优化
- 07.线上内存泄漏检测方案
- 7.1 LeakCanary为何不能用于线上
- 7.2 KOOM线上检测方案
- 7.3 线上内存泄漏监控体系
- 7.4 各方案对比分析
- 08.常见面试题深度解析
- 8.1 经典面试问题
- 8.2 进阶面试问题
- 8.3 学习建议
# 01.整体概述介绍
# 1.1 项目背景介绍
在Android应用开发中,内存泄漏是最常见且危害极大的性能问题之一。内存泄漏会导致应用可用内存逐渐减少,当堆积到一定程度时就会触发频繁GC甚至OOM崩溃,严重影响用户体验。
然而内存泄漏的排查一直是一个困难的工作。传统方式需要开发者手动使用Android Profiler或MAT工具分析hprof文件,这个过程非常耗时且需要专业知识。而且很多内存泄漏在开发测试阶段不容易被发现,往往到线上才暴露问题。
LeakCanary是Square公司开源的Android内存泄漏检测库,它能够自动检测应用中的内存泄漏,并以友好的方式展示泄漏的引用链,大大降低了内存泄漏排查的门槛和成本。
LeakCanary的版本演进:
版本演进:
LeakCanary 1.x(2015年发布)
├── 基于haha库分析hprof文件
├── 需要手动在build.gradle中添加依赖
├── 需要在Application中初始化
└── 分析速度较慢
LeakCanary 2.x(2019年发布)
├── 使用全新的Shark分析引擎,替换haha
├── 完全用Kotlin重写
├── 利用ContentProvider实现自动初始化(零配置)
├── 分析速度提升约6倍
├── 支持更多泄漏场景检测
└── 更友好的UI展示
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.2 内存泄漏概念
什么是内存泄漏? 当App无法释放不再需要的对象引用时,即为内存泄漏。也可以理解为:生命周期长的对象持有了生命周期短的对象引用,导致短生命周期对象无法被GC回收。
内存泄露(Memory Leaks)指不再使用的对象或数据没有被回收,随着内存泄漏的堆积,应用性能会逐渐变差,甚至发生OOM崩溃。
应用中的内存泄漏可以分为两类:
- Java内存泄露:不再使用的对象被生命周期更长的GC Root引用,无法被判定为垃圾对象而导致内存泄漏。LeakCanary主要监控Java内存泄漏。
- Native内存泄露:Native内存没有垃圾回收机制,需要手动管理,未手动回收导致内存泄漏。
常见的内存泄漏场景:
常见内存泄漏类型:
1. Activity/Fragment泄漏
├── 非静态内部类持有外部类引用(如Handler、AsyncTask)
├── 匿名内部类持有Activity引用(如Listener、Callback)
├── 单例模式持有Activity Context
└── 静态变量持有Activity引用
2. 资源未关闭
├── Cursor未关闭
├── Stream未关闭
├── TypedArray未回收
└── Bitmap未回收
3. 注册未注销
├── BroadcastReceiver未注销
├── EventBus未反注册
├── 观察者模式未移除Observer
└── ContentObserver未注销
4. 集合类泄漏
├── 静态集合类只增不减
├── HashMap的Key对象修改hashCode
└── 全局缓存无淘汰机制
5. WebView泄漏
└── WebView持有Activity引用,销毁时需特殊处理
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1.3 设计目标
LeakCanary支持以下五种Android场景中的内存泄漏监测:
- 已销毁的Activity对象(进入DESTROYED状态)
- 已销毁的Fragment对象和Fragment View对象(进入DESTROYED状态)
- 已清除的ViewModel对象(进入CLEARED状态)
- 已销毁的Service对象(进入DESTROYED状态)
- 已从WindowManager中移除的RootView对象
疑惑:为什么只监控这五种场景?
这五种场景覆盖了Android应用中最常见且影响最大的内存泄漏类型。Activity和Fragment是最重量级的UI组件,泄漏它们意味着整个视图树都无法回收。ViewModel和Service是常被长生命周期对象引用的组件。RootView的泄漏则可能导致整个Window无法回收。
论证:为什么不监控所有对象?
如果监控所有对象,会带来极大的性能开销。每个被监控对象都需要创建WeakReference和维护引用队列,大量对象的监控会导致频繁GC和内存抖动。LeakCanary选择重点监控这五类高价值目标,在检测效果和性能开销之间取得了最佳平衡。
# 1.4 产生收益
LeakCanary的核心优势:
- 自动化检测:不需要手动触发,应用运行过程中自动检测
- 零配置:只需添加依赖,无需任何初始化代码(基于ContentProvider自动初始化)
- 精确定位:提供完整的引用链,精确到泄漏对象被哪个GC Root持有
- 友好展示:通过通知栏提醒和专门的UI页面展示泄漏信息
- 开发效率:大大缩短内存泄漏的排查时间,从数小时缩短到分钟级别
# 02.开发设计思路
# 2.1 整体设计思路
LeakCanary的核心工作流程可以概括为以下5个阶段:
LeakCanary核心工作流程:
阶段1:注册无用对象监听
└── 在Android Framework中注册监听器
└── 感知五种泄漏场景中产生无用对象的时机
└── 如Activity.onDestroy()后,产生一个无用Activity对象
阶段2:监控内存泄漏
└── 为无用对象关联WeakReference + ReferenceQueue
└── 等待5秒后检查弱引用是否进入引用队列
└── 如果没有进入,则认为对象发生泄漏
└── 泄漏对象计数达到阈值才触发分析
阶段3:Heap Dump
└── 调用Debug.dumpHprofData()生成hprof文件
└── Dump过程会锁堆,应用短暂冻结
└── 生成的hprof文件通常10+MB
阶段4:分析堆快照
└── 使用Shark引擎分析hprof文件
└── 在独立线程或独立进程中执行
└── 查找泄漏对象到GC Root的最短引用链
阶段5:输出分析报告
└── Logcat打印分析结果
└── 发送系统通知
└── 可视化报告页面展示
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 2.2 初始化思路
LeakCanary 2.x利用ContentProvider实现零配置自动初始化,这是一个非常巧妙的设计。
疑惑:为什么不需要在Application中手动初始化? ContentProvider的onCreate()在Application.onCreate()之前调用。Android应用启动时组件初始化顺序:
Application.attachBaseContext()
↓
ContentProvider.onCreate() ← LeakCanary在这里初始化
↓
Application.onCreate()
↓
Activity.onCreate()
2
3
4
5
6
7
LeakCanary在AndroidManifest中注册了一个名为AppWatcherInstaller的ContentProvider,系统在创建Application时会自动创建这个ContentProvider,从而触发LeakCanary的初始化逻辑。
// LeakCanary自动初始化的关键代码
internal sealed class AppWatcherInstaller : ContentProvider() {
internal class MainProcess : AppWatcherInstaller()
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
// 安装LeakCanary
AppWatcher.manualInstall(application)
return true
}
}
2
3
4
5
6
7
8
9
10
11
初始化做了什么:
- 注册Activity生命周期监听(通过registerActivityLifecycleCallbacks)
- 注册Fragment生命周期监听
- 注册ViewModel清除监听
- 注册Service销毁监听(通过Hook)
- 注册RootView移除监听(通过Hook)
# 2.3 设计组件监听
不同组件的监听策略不同,因为Android Framework提供的监听接口各不相同:
Activity监控: 通过Application.registerActivityLifecycleCallbacks()接口监听Activity的onDestroy事件。将当前Activity对象交给ObjectWatcher监控
Fragment与Fragment View监控:
- 首先通过
Application.registerActivityLifecycleCallbacks()接口监听Activity的onCreate事件 - 再通过
FragmentManager.registerFragmentLifecycleCallbacks()接口监听Fragment的生命周期 - 在
onFragmentViewDestroyed()中监控Fragment View - 在
onFragmentDestroyed()中监控Fragment
ViewModel监控:
- Android Framework未提供ViewModel.onClear()全局监听方法
- LeakCanary通过Hook方式实现
- 在Activity.onCreate和Fragment.onCreate事件中实例化一个自定义ViewModel
- 在ViewModel.onClear()方法中,通过反射获取当前作用域中所有的ViewModel对象
- 将这些ViewModel对象交给ObjectWatcher监控
ViewModel监控的Hook原理:
Activity/Fragment.onCreate()
↓
ViewModelProvider(owner).get(ViewModelClearedWatcher::class)
→ 创建自定义的ViewModelClearedWatcher
↓
当Activity/Fragment销毁时
↓
ViewModelClearedWatcher.onCleared()
↓
反射获取ViewModelStore中所有ViewModel
→ viewModelStore.keys().forEach { key ->
val viewModel = ViewModelStore.get(key)
objectWatcher.expectWeaklyReachable(viewModel, ...)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Service监控:
- Android Framework未提供Service.onDestroy()全局监听方法
- 需要两步Hook来实现:
- Hook主线程消息循环的
mH.mCallback回调,监听STOP_SERVICE消息,暂存即将销毁的Service对象 - 使用动态代理Hook
IActivityManagerBinder对象,代理serviceDoneExecuting()方法,视为Service.onDestroy()的执行时机
- Hook主线程消息循环的
RootView监控:
- 通过Hook
WindowManagerGlobal.mViewsRootView列表获取RootView新增和移除的时机 - 检查View对应的Window类型(Dialog、DreamService等)
- 注册
View.addOnAttachStateChangeListener()监听 - 在
onViewDetachedFromWindow()回调中将View对象交给Watcher监控
# 2.4 无用对象监听
如何标记一个对象并判断其是否泄漏?核心原理是利用Java的 WeakReference + ReferenceQueue机制。
原理说明: 为弱引用指定一个引用队列,当弱引用指向的对象被GC回收时,此弱引用就会被添加到这个队列中。通过判断引用队列中有没有这个弱引用,就能判断该弱引用指向的对象是否被回收了。
// 演示WeakReference + ReferenceQueue的工作原理
ReferenceQueue<Object> queue = new ReferenceQueue<>();
private void test() {
Object obj = new Object();
// 创建弱引用,关联引用队列
WeakReference<Object> reference = new WeakReference<>(obj, queue);
System.out.println("弱引用对象: " + reference);
// GC前,queue为空
System.gc();
printlnQueue("GC前(obj未置null)"); // 输出: 空
// 置空obj,使其成为GC候选对象
obj = null;
System.gc();
printlnQueue("GC后(obj已置null)"); // 输出: 弱引用对象
}
private void printlnQueue(String tag) {
Object obj;
while ((obj = queue.poll()) != null) {
System.out.println(tag + ": " + obj);
}
}
// 输出结果:
// 弱引用对象: java.lang.ref.WeakReference@6e0be858
// GC前(obj未置null):(空)
// GC后(obj已置null): java.lang.ref.WeakReference@6e0be858
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
利用这个特性,可以检测Activity的内存泄漏:
- Activity在onDestroy()之后应该被销毁
- 用WeakReference指向Activity,并关联ReferenceQueue
- 等待GC后检查引用队列是否包含该弱引用
- 如果不包含,说明Activity没有被回收,可能发生了泄漏
# 2.5 内存泄漏监听
LeakCanary的泄漏判定流程分为两层:
第一层:注册无用对象监听。在Android Framework中注册全局监听器或者Hook,在对象进入无用状态时(如Activity.onDestroy())将其交给ObjectWatcher监控。
第二层:利用引用对象感知垃圾回收。为无用对象包装KeyedWeakReference,并在一段时间后(默认5秒)观察弱引用是否如期进入关联的引用队列。
// ObjectWatcher核心逻辑简化版
class ObjectWatcher {
private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>()
fun expectWeaklyReachable(watchedObject: Any, description: String) {
val key = UUID.randomUUID().toString()
val reference = KeyedWeakReference(watchedObject, key, description, queue)
watchedObjects[key] = reference
// 5秒后检查
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
private fun moveToRetained(key: String) {
// 先清理已回收的对象
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
// 对象仍在监控Map中 → 疑似泄漏
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
private fun removeWeaklyReachableObjects() {
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key) // 已被回收,移除监控
}
} while (ref != null)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
LeakCanary发现泄漏对象后就会立刻触发分析吗?
不会。LeakCanary不会每次发现泄漏对象都进行分析,而是有两个拦截条件:
- 拦截1:泄漏对象计数未达到阈值(默认5个),或者进入后台时间未达到阈值
- 拦截2:距离上一次HeapDump未超过60秒
这样设计是因为HeapDump和分析过程非常耗时,频繁执行会严重影响用户体验。
# 2.6 Dump内存
当泄漏对象计数达到阈值时,LeakCanary会触发HeapDump操作。HeapDump触发流程:
泄漏对象计数 >= 5
↓
检查距离上次Dump是否超过60秒
├── 否 → 等待
└── 是 ↓
触发GC(Runtime.getRuntime().gc())
↓
等待100ms
↓
再次检查泄漏对象(移除已回收的)
↓
如果仍有泄漏对象 → 执行Dump
↓
调用Debug.dumpHprofData(filePath)
└── 该方法会锁堆(Stop-The-World)
└── 生成.hprof文件(通常10-30MB)
↓
发送HeapDump事件
↓
开启前台服务进行分析
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Dump的两种触发策略:
- 自动触发:泄漏对象计数达到阈值(默认5个)时自动触发
- 手动触发:用户点击通知栏提前触发分析
# 2.7 分析内存快照
LeakCanary 2.x使用自研的Shark引擎替代了1.x时代的haha库来分析hprof文件。Shark分析流程:
hprof文件分析流程:
1. 解析文件头
└── 读取hprof文件格式头信息
└── 获取解析开始位置
2. 构建内存索引
└── 扫描hprof文件中的所有Record
└── 建立对象ID到文件偏移的映射
└── 使用两次遍历策略(第一次建索引,第二次按需读取)
3. 构建对象图(Graph)
└── 基于索引按需加载对象信息
└── 避免一次性加载整个堆到内存
└── 使用Lazy Loading策略
4. 查找泄漏路径
└── 从泄漏对象出发
└── 使用广度优先搜索(BFS)
└── 查找到GC Root的最短引用链
└── 考虑引用类型(强引用、软引用、弱引用)
5. 生成分析报告
└── 输出引用链
└── 标记怀疑对象
└── 分组相同泄漏
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
疑惑:为什么Shark比haha快6倍?
答疑:
- haha将整个hprof文件加载到内存,内存消耗巨大;Shark使用索引+按需加载,内存消耗小得多
- haha使用Java的反射机制解析;Shark使用高效的Okio进行IO操作
- Shark对hprof文件只做两次遍历(建索引+分析),而haha需要多次遍历
- Shark针对Android的hprof格式做了专门优化
# 2.8 输出堆栈链报告
LeakCanary生成的泄漏报告包含以下关键信息,泄漏报告示例:
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread
│ Leaking: NO (it's a system class)
│ ↓ ActivityThread.mActivities
│ ~~~~~~~~~~~
├─ android.util.ArrayMap
│ Leaking: NO
│ ↓ ArrayMap.mArray
│ ~~~~~~
├─ java.lang.Object[]
│ Leaking: NO
│ ↓ Object[1]
│ ~~~~~~~
├─ android.app.ActivityThread$ActivityClientRecord
│ Leaking: NO
│ ↓ ActivityClientRecord.activity
│ ~~~~~~~~
╰→ com.example.MainActivity
Leaking: YES (Activity is destroyed)
key = "xxx-xxx-xxx"
报告中的关键信息:
├── Leaking: YES/NO → 是否是泄漏对象
├── ~~~下划线~~~ → 怀疑泄漏路径
├── key → 泄漏对象的唯一标识
└── 引用链从GC Root到泄漏对象
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 2.9 KOOM设计思路
快手技术团队开源的KOOM(Kwai OOM)框架针对LeakCanary的性能瓶颈做了优化。
核心优化:利用Copy-on-Write思想,fork子进程进行HeapDump
LeakCanary的Dump问题:
Debug.dumpHprofData() → 锁堆(Stop-The-World)
→ 应用冻结数秒
→ 用户体验极差
→ 不适合线上使用
KOOM的优化方案:
fork()子进程 → 子进程继承父进程内存快照(COW)
→ 子进程中执行Debug.dumpHprofData()
→ 父进程(主应用)不受影响
→ 适合线上使用
COW (Copy-On-Write) 原理:
fork()后父子进程共享同一物理内存页
→ 只有写操作时才会复制对应的内存页
→ fork本身非常快(只复制页表)
→ 子进程获得了父进程内存的完整快照
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
KOOM还做了以下优化:
- 使用内存阈值触发检测(而非对象计数)
- 优化hprof文件裁剪,减小文件体积
- 支持线上环境使用
# 03.LeakCanary原理
# 3.1 思考一些问题
在深入源码之前,先思考几个问题:
核心问题:
1. LeakCanary的核心工作流程是什么?设计思想是什么?
2. 初始化做了哪些工作?如何做到零配置?
3. 如何监听各种组件的销毁事件?尤其是ViewModel、Service?
4. 如何判断对象是否泄漏?判定条件是什么?
5. WeakReference + ReferenceQueue的检测机制原理?
6. 检测出泄漏后如何生成泄漏信息?引用链如何得到?
7. 为什么不能用于线上?有什么替代方案?
2
3
4
5
6
7
8
原理简单介绍:
通过监听组件的生命周期(Activity.onDestroy等),在组件销毁后手动触发GC,然后通过ReferenceQueue + WeakReference来判断对象是否被回收。如果GC后对象仍未被回收,则进行HeapDump生成hprof文件,再通过Shark引擎分析泄漏的引用链。
# 3.2 原理流程概括
如何触发检测:
LeakCanary基于LeakSentry开发,LeakSentry会Hook Android生命周期,自动检测Activity或Fragment被销毁时实例是否被回收。销毁的实例传给ObjectWatcher(即老版的RefWatcher),ObjectWatcher持有它们的弱引用。等待5秒,如果GC触发后弱引用还没有被清理,则认为可能发生内存泄漏。
判断是否存在内存泄漏:
- 尝试从ReferenceQueue中获取待分析对象对应的弱引用
- 如果弱引用已在队列中,说明对象正在被回收 → 返回DONE
- 如果不在队列中,可能存在泄漏 → 手动触发GC
- GC后再次检查引用队列
- 如果仍不在队列中 → 确认泄漏
分析内存泄漏:
确认泄漏后,调用heapDumper.dumpHeap()生成hprof文件,Shark引擎分析hprof文件查找从泄漏对象到GC Root的最短引用链。
整体流程图:
组件销毁(onDestroy等)
↓
ObjectWatcher.expectWeaklyReachable(object)
↓
创建KeyedWeakReference + ReferenceQueue
↓
等待5秒
↓
检查ReferenceQueue
├── 弱引用在队列中 → 对象已回收 → 移除监控(正常)
└── 弱引用不在队列中 → 触发GC → 再次检查
├── 在队列中 → 移除监控(正常)
└── 不在队列中 → 确认泄漏 → 泄漏计数+1
↓
泄漏计数 >= 阈值?
├── 否 → 发送通知提醒
└── 是 → HeapDump → Shark分析 → 输出报告
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.3 初始化流程
LeakCanary 2.x的初始化通过ContentProvider自动完成:
// AppWatcherInstaller 在 AndroidManifest 中自动注册
internal sealed class AppWatcherInstaller : ContentProvider() {
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
}
// AppWatcher.manualInstall() 完成初始化
fun manualInstall(
application: Application,
retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {
// 1. 安装各种Watcher
watchersToInstall.forEach { watcher ->
watcher.install()
}
}
// 默认安装的5种Watcher
fun appDefaultWatchers(application: Application): List<InstallableWatcher> {
return listOf(
ActivityWatcher(application, reachabilityWatcher),
FragmentAndViewModelWatcher(application, reachabilityWatcher),
RootViewWatcher(reachabilityWatcher),
ServiceWatcher(reachabilityWatcher)
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 3.4 监听组件销毁
Activity监听 - ActivityWatcher:
内部注册了Activity的全局生命周期监听,在onDestroy()时追踪当前Activity对象:
class ActivityWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {
private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks
by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy()"
)
}
}
override fun install() {
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Fragment监听 - FragmentAndViewModelWatcher:
先注册Activity生命周期监听,然后在Activity.onCreate()时注册Fragment的生命周期监听:
// 在Activity.onCreate中注册Fragment监听
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
val fragmentManager = activity.fragmentManager
fragmentManager.registerFragmentLifecycleCallbacks(object :
FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) {
// 监控Fragment的View
val view = f.view
if (view != null) {
reachabilityWatcher.expectWeaklyReachable(view, ...)
}
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
// 监控Fragment自身
reachabilityWatcher.expectWeaklyReachable(f, ...)
}
}, true)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ViewModel监听:
通过在作用域中创建自定义ViewModel,利用其onCleared()回调来获取所有ViewModel的清除时机:
// 自定义ViewModel用于监听其他ViewModel的清除
class ViewModelClearedWatcher(
storeOwner: ViewModelStoreOwner,
private val reachabilityWatcher: ReachabilityWatcher
) : ViewModel() {
private val viewModelMap: Map<String, ViewModel>? = try {
// 反射获取ViewModelStore内部的map
val storeField = ViewModelStore::class.java.getDeclaredField("map")
storeField.isAccessible = true
storeField.get(storeOwner.viewModelStore) as Map<String, ViewModel>
} catch (ignored: Exception) { null }
override fun onCleared() {
// 当前作用域的ViewModel被清理时
viewModelMap?.values?.forEach { viewModel ->
reachabilityWatcher.expectWeaklyReachable(
viewModel, "${viewModel::class.java.name} received ViewModel#onCleared()"
)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Service监听 - ServiceWatcher:
由于Android Framework未提供Service.onDestroy()的全局监听接口,LeakCanary通过两步Hook实现:
Service监控的双Hook方案:
Hook 1: 监听ActivityThread.mH.mCallback中的STOP_SERVICE消息
→ 在收到STOP_SERVICE消息时
→ 通过反射从ActivityThread.mServices中取出Service对象
→ 暂存到一个WeakHashMap中
Hook 2: 动态代理IActivityManager
→ 代理serviceDoneExecuting()方法
→ 该方法在Service.onDestroy()执行后被调用
→ 从暂存Map中取出Service对象
→ 交给ObjectWatcher监控
2
3
4
5
6
7
8
9
10
11
12
# 3.5 监听无用对象
ObjectWatcher是LeakCanary的核心组件,负责管理所有被监控对象的生命周期:
ObjectWatcher内部数据结构:
watchedObjects: Map<String, KeyedWeakReference>
├── key: UUID随机生成的唯一标识
└── value: KeyedWeakReference(继承WeakReference)
queue: ReferenceQueue<Any>
└── 当WeakReference指向的对象被回收时
└── 该WeakReference会自动进入此队列
KeyedWeakReference:
├── key: String(唯一标识)
├── description: String(描述信息)
├── watchUptimeMillis: Long(开始监控的时间)
├── retainedUptimeMillis: Long(确认泄漏的时间,0表示未泄漏)
└── 继承自WeakReference<Any>,关联ReferenceQueue
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
判断逻辑详细说明:
- 对要监听的对象,使用KeyedWeakReference与其关联(初始化时传入引用队列queue),并保存到watchedObjects Map中
- 使用Handler延迟5秒后执行判断
- 判断时先遍历ReferenceQueue,将已回收对象从watchedObjects中移除
- 再检查目标对象是否仍在watchedObjects中
- 如果仍在,调用
Runtime.getRuntime().gc()触发GC - GC后等待100ms,再次执行步骤3-4
- 如果对象仍然存在 → 确认泄漏
# 3.6 监控内存泄漏
LeakCanary确认泄漏后的处理策略:
// 泄漏对象计数逻辑
private fun checkRetainedObjects() {
val retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount == 0) return
// 拦截1: 距离上次Dump不足60秒
if (System.currentTimeMillis() - lastHeapDumpTime < 60_000) {
scheduleRetainedObjectCheck()
return
}
// 拦截2: 泄漏计数未达阈值
if (retainedReferenceCount < retainedVisibleThreshold) {
// 发送通知提醒,但不触发Dump
showRetainedCountNotification(retainedReferenceCount)
scheduleRetainedObjectCheck()
return
}
// 执行HeapDump
dumpHeap(retainedReferenceCount)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.7 Dump内存快照
在判定有内存泄漏后,LeakCanary调用系统提供的Debug.dumpHprofData(File)函数,生成虚拟机的内存快照(hprof文件)。
Dump流程:
1. 触发条件满足 → 开始Dump
2. 调用Debug.dumpHprofData(file)
└── 该方法会暂停所有线程(Stop-The-World)
└── 遍历Java堆中的所有对象
└── 将对象信息写入hprof文件
└── 恢复所有线程
3. 生成hprof文件(通常10-30MB)
4. 将泄漏对象的referenceKey和hprof文件封装为HeapDump对象
5. 发送LeakCanary内部事件
6. 开启前台服务执行分析工作
2
3
4
5
6
7
8
9
10
11
12
# 3.8 分析堆快照
LeakCanary 2.x使用Shark引擎分析hprof文件。
分析流程:
Shark分析hprof文件的步骤:
Step 1: 解析文件头
└── 读取hprof文件版本、时间戳等元信息
└── 确定解析起始位置
Step 2: 构建内存索引(第一次遍历)
└── 扫描所有Record类型
└── 记录每个对象的ID和在文件中的偏移量
└── 建立高效的索引数据结构
└── 不将对象数据加载到内存
Step 3: 构建对象图(按需)
└── 使用索引按需读取对象信息
└── 建立对象间的引用关系图
└── 识别GC Root对象
Step 4: 查找泄漏路径(BFS广度优先搜索)
└── 从所有GC Root出发
└── 广度优先遍历对象图
└── 找到到达泄漏对象的最短路径
└── 记录路径上的每个引用节点
Step 5: 生成报告
└── 格式化引用链
└── 标记Leaking YES/NO
└── 计算泄漏签名
└── 分组相同泄漏
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 3.9 输出分析报告
分析完成后,LeakCanary通过多种方式输出结果:
- Logcat输出:在控制台打印完整的引用链信息
- 系统通知:发送通知栏消息,点击可跳转到详情页
- 可视化页面:LeakCanary提供了专门的Activity展示泄漏信息
- 桌面快捷方式:生成快捷入口方便查看
# 04.一些技术点思考
# 4.1 WeakReference与ReferenceQueue机制
Java中有四种引用类型,LeakCanary利用了WeakReference的特性:
Java四种引用类型:
强引用 (Strong Reference)
Object obj = new Object();
└── GC时不会回收,即使OOM也不回收
└── 这是内存泄漏的根本原因
软引用 (SoftReference)
SoftReference<Object> soft = new SoftReference<>(obj);
└── 内存不足时才会回收
└── 适合做内存敏感的缓存
弱引用 (WeakReference) ← LeakCanary使用
WeakReference<Object> weak = new WeakReference<>(obj, queue);
└── 只要发生GC就会回收(如果没有强引用指向)
└── 可以关联ReferenceQueue
└── 对象被回收时,WeakReference自动入队
虚引用 (PhantomReference)
PhantomReference<Object> phantom = new PhantomReference<>(obj, queue);
└── 随时可能被回收
└── 必须配合ReferenceQueue使用
└── get()方法永远返回null
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ReferenceQueue的工作原理:
ReferenceQueue工作原理:
1. 创建WeakReference时关联ReferenceQueue
WeakReference ref = new WeakReference(obj, queue)
2. 当obj被GC回收时
JVM将ref放入queue中(由GC线程完成)
3. 应用线程通过queue.poll()获取已入队的引用
→ 如果返回非null,说明对应的对象已被回收
→ 如果返回null,说明没有新的对象被回收
LeakCanary的使用方式:
创建WeakReference → 关联queue → 5秒后poll queue
→ 如果该WeakReference在queue中 → 对象已回收(正常)
→ 如果不在queue中 → 对象未回收(可能泄漏)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4.2 引用链如何生成
泄漏对象的引用链是分析报告的核心信息。LeakCanary使用广度优先搜索(BFS)算法在对象图中查找最短引用链:
引用链生成算法(BFS):
输入:
- GC Root对象集合
- 目标泄漏对象
算法:
queue = [所有GC Root]
visited = {}
parent = {} // 记录每个对象的前驱节点
while queue is not empty:
current = queue.dequeue()
if current == 泄漏对象:
return reconstructPath(parent, current) // 重建路径
for each reference in current.references:
child = reference.target
if child not in visited:
visited.add(child)
parent[child] = (current, reference.name)
queue.enqueue(child)
return null // 未找到路径
路径重建:
从泄漏对象沿着parent链回溯到GC Root
→ 得到GC Root → ... → 泄漏对象的完整引用链
→ 因为是BFS,保证是最短路径
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
为什么选择BFS而不是DFS?
BFS能保证找到的是最短引用链,这对于开发者排查问题非常重要。最短引用链意味着最直接的泄漏原因,减少了排查的干扰信息。
# 4.3 Shark分析引擎原理
Shark(Smart Heap Analysis Reports for Kotlin)是LeakCanary 2.x专门开发的hprof分析引擎:
Shark vs haha 对比:
haha (LeakCanary 1.x):
├── 基于Android Studio的hprof解析器
├── 一次性加载整个hprof到内存
├── 使用HashMap存储对象信息
├── 内存消耗大,分析慢
└── 不再维护
Shark (LeakCanary 2.x):
├── 全新设计,使用Kotlin编写
├── 两次遍历策略(建索引+按需读取)
├── 使用Okio进行高效IO操作
├── 内存消耗小(按需加载)
├── 分析速度快约6倍
└── 支持更丰富的分析能力
Shark的两次遍历策略:
第一次遍历:建立索引
→ 只记录每个对象的ID和文件偏移
→ 不读取对象的具体字段数据
→ 索引数据结构紧凑,内存开销小
第二次遍历(按需):
→ BFS搜索时,需要某个对象的引用信息
→ 根据索引直接seek到文件对应位置读取
→ 避免将整个堆加载到内存
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 4.4 提高Dump分析效率
Dump分析是十分耗时的行为。由于LeakCanary分析堆快照的过程存在一定的内存消耗,整个分析过程一般会持续几十秒,对于一些性能差的机型会造成明显的卡顿甚至ANR。
优化方案:
- 多进程分析:将分析工作放到独立进程中,不影响主进程性能
- 线程异步执行:即使在同一进程中,也使用后台线程执行
- 按需加载:Shark引擎不一次性加载整个hprof到内存
- 缓存机制:对相同泄漏模式的结果进行缓存
分析策略选择:
LeakCanary会根据依赖项选择策略:
├── 如果添加了leakcanary-android-process依赖
│ └── 使用独立进程(:leakcanary)进行分析
│ └── 完全不影响主进程
├── 否则
│ └── 使用后台Thread进行分析
│ └── 开启前台Service保证不被杀
2
3
4
5
6
7
8
9
# 4.5 相同问题分组
LeakCanary会将相同问题重复触发的内存泄漏进行分组,减少重复排查工作。
分组方法:按引用链的签名
泄漏签名计算方式:
引用链签名 = hash(每个引用节点的类型拼接)
示例:
泄漏1的引用链:
ActivityThread → ArrayMap → Object[] → ActivityClientRecord → MainActivity
签名 = hash("ActivityThread.ArrayMap.Object[].ActivityClientRecord.MainActivity")
泄漏2的引用链(同一个泄漏触发第二次):
ActivityThread → ArrayMap → Object[] → ActivityClientRecord → MainActivity
签名 = hash("ActivityThread.ArrayMap.Object[].ActivityClientRecord.MainActivity")
签名相同 → 归为同一组 → 只需排查一次
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4.6 如何标记怀疑对象
为了提高排查效率,LeakCanary会自动帮助缩小排查范围:
标记策略:
1. 已知的系统级对象 → Leaking: NO
如ActivityThread、WindowManagerGlobal等
这些对象本身就有全局生命周期,不是泄漏原因
2. 目标泄漏对象 → Leaking: YES
已确认被销毁但未被回收的对象
如已调用onDestroy()的Activity
3. 怀疑对象 → 用~~~标记
位于Leaking: NO和Leaking: YES之间的对象
这些对象就是需要开发者重点排查的
示例:
├─ android.app.ActivityThread Leaking: NO
│ ↓ ActivityThread.mHandler
├─ android.os.Handler Leaking: NO
│ ↓ Handler.mCallback
│ ~~~~~~~~~ ← 怀疑点
├─ com.example.MyCallback Leaking: UNKNOWN
│ ↓ MyCallback.activity
│ ~~~~~~~~ ← 怀疑点
╰→ com.example.MainActivity Leaking: YES
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 05.优秀代码设计解析
# 5.1 设计模式在LeakCanary中的应用
LeakCanary的源码中运用了多种经典设计模式,值得深入学习:
LeakCanary中的设计模式:
1. 观察者模式(核心)
ObjectWatcher作为被观察者,各种Watcher作为观察者
→ OnObjectRetainedListener接口:观察泄漏事件
→ 当泄漏对象被确认时,通知所有Listener
→ 解耦了泄漏检测和泄漏处理逻辑
// 观察者接口
fun interface OnObjectRetainedListener {
fun onObjectRetained()
}
// 注册观察者
objectWatcher.addOnObjectRetainedListener(listener)
// 通知观察者(在moveToRetained中)
onObjectRetainedListeners.forEach { it.onObjectRetained() }
2. 策略模式
不同组件的监听策略不同,通过InstallableWatcher接口统一:
→ ActivityWatcher:使用ActivityLifecycleCallbacks策略
→ FragmentAndViewModelWatcher:使用FragmentLifecycleCallbacks策略
→ ServiceWatcher:使用Hook ActivityThread策略
→ RootViewWatcher:使用Hook WindowManager策略
每种Watcher的install/uninstall逻辑完全不同
但对外提供统一的InstallableWatcher接口
→ 新增监控类型只需实现新的Watcher,不影响已有代码
3. 工厂方法模式
HeapDumper接口定义了Dump操作的抽象:
→ AndroidDebugHeapDumper:使用Debug.dumpHprofData()
→ 可以替换为自定义实现(如KOOM的fork方案)
→ 解耦Dump策略和使用方
4. 模板方法模式
ContentProvider的onCreate作为模板方法:
→ AppWatcherInstaller定义初始化模板
→ MainProcess和LeakCanaryProcess是两种不同的初始化流程
→ sealed class实现,编译时确定子类
5. 建造者模式
AppWatcher.Config通过DSL风格的建造者构建:
AppWatcher.config = AppWatcher.config.copy(
retainedObjectTracker = ...,
watchDurationMillis = 5000
)
→ Kotlin的data class + copy()天然支持建造者模式
→ 不可变对象设计,线程安全
6. 门面模式(Facade)
AppWatcher和LeakCanary类是门面:
→ 对外只暴露少量简洁API
→ 内部协调ObjectWatcher、HeapDumpTrigger、Shark等复杂组件
→ 降低使用者的学习成本
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 5.2 sealed class的巧妙使用
LeakCanary使用Kotlin的sealed class实现类型安全的层次结构,这是非常优秀的代码设计:
// 自动初始化的sealed class设计
internal sealed class AppWatcherInstaller : ContentProvider() {
// 主进程初始化
internal class MainProcess : AppWatcherInstaller()
// LeakCanary独立进程初始化
internal class LeakCanaryProcess : AppWatcherInstaller()
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
// ContentProvider的其他方法返回默认值(空实现)
override fun query(...) = null
override fun getType(...) = null
override fun insert(...) = null
override fun delete(...) = 0
override fun update(...) = 0
}
// 设计精妙之处:
// 1. sealed class限制了子类范围(只有MainProcess和LeakCanaryProcess)
// 2. 在Manifest中根据进程选择不同的ContentProvider子类
// 3. 利用ContentProvider的自动创建特性实现零配置
// 4. internal修饰符防止外部继承和使用
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// LeakTrace中对泄漏状态的sealed class建模
sealed class LeakingStatus {
object YES : LeakingStatus()
object NO : LeakingStatus()
object UNKNOWN : LeakingStatus()
}
// 引用链节点也使用sealed class
sealed class LeakTraceReference {
data class InstanceFieldReference(val declaringClassName: String, val fieldName: String)
data class StaticFieldReference(val declaringClassName: String, val fieldName: String)
data class ArrayReference(val index: Int)
// ...
}
// 优势:
// → when表达式编译时检查分支完整性
// → 比enum更灵活,每个子类可以有不同字段
// → 比普通继承更安全,限制了子类范围
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 5.3 委托模式与扩展函数的运用
// noOpDelegate() — 委托模式的精妙运用
// 只需关注onActivityDestroyed,其他回调不需要实现
private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy()"
)
}
}
// noOpDelegate()的实现:使用动态代理创建空实现
inline fun <reified T : Any> noOpDelegate(): T {
val javaClass = T::class.java
return Proxy.newProxyInstance(
javaClass.classLoader,
arrayOf(javaClass)
) { _, _, _ ->
// 所有方法返回默认值
} as T
}
// 设计优势:
// 1. 避免实现大量不需要的回调方法(ActivityLifecycleCallbacks有7个方法)
// 2. by关键字实现接口委托,比Java的匿名内部类更简洁
// 3. 只override需要的方法,代码意图更清晰
// 4. 通用的noOpDelegate可以用于任何接口
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 5.4 KeyedWeakReference的设计
// KeyedWeakReference — 核心数据结构设计
class KeyedWeakReference(
referent: Any, // 被监控的对象
val key: String, // 唯一标识(UUID)
val description: String, // 描述信息
val watchUptimeMillis: Long, // 开始监控时间
referenceQueue: ReferenceQueue<Any> // 关联的引用队列
) : WeakReference<Any>(referent, referenceQueue) {
@Volatile
var retainedUptimeMillis = -1L // 确认泄漏的时间(-1表示未泄漏)
companion object {
@Volatile
@JvmStatic
var heapDumpUptimeMillis = 0L // 最近一次HeapDump时间
}
}
// 设计思考:
// Q: 为什么需要key字段?
// A: WeakReference被回收后,referent(被监控对象)变为null
// 无法通过对象本身来标识是哪个引用
// 需要key作为唯一标识在watchedObjects Map中查找
// 同时ReferenceQueue中的引用也需要通过key来匹配
// Q: 为什么retainedUptimeMillis用@Volatile?
// A: 该字段可能在多个线程间读写:
// 检测线程设置值,主线程/分析线程读取值
// @Volatile保证可见性
// Q: 为什么使用uptimeMillis而不是System.currentTimeMillis?
// A: uptimeMillis不受系统时间修改影响
// 避免用户手动改时间导致判断逻辑出错
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 5.5 Clock抽象与可测试性设计
// LeakCanary的时间抽象 — 可测试性的典范
fun interface Clock {
fun uptimeMillis(): Long
}
// 生产环境使用真实时钟
val realClock = Clock { SystemClock.uptimeMillis() }
// 测试环境使用可控时钟
class FakeClock : Clock {
var currentTime = 0L
override fun uptimeMillis() = currentTime
fun advance(millis: Long) { currentTime += millis }
}
// 使用方式:
class ObjectWatcher(
private val clock: Clock,
private val checkRetainedExecutor: Executor,
...
) {
fun expectWeaklyReachable(watchedObject: Any, description: String) {
val watchUptimeMillis = clock.uptimeMillis()
// ...
}
}
// 设计优势:
// 1. 依赖注入:通过构造函数注入Clock,不硬编码系统调用
// 2. 可测试性:测试时注入FakeClock,精确控制时间流逝
// 3. fun interface:SAM接口,可用lambda创建,极简
// 4. 这是整洁架构中"依赖倒置原则"的体现
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 5.6 Shark引擎中的图遍历算法设计
Shark内存图遍历的设计亮点:
1. 两次遍历策略(Two-Pass Strategy)
第一次遍历:只建立索引
→ 记录每个对象的ID和文件偏移量
→ 使用紧凑的数据结构(如LongToLongMap)
→ 内存开销极小
第二次遍历:按需读取
→ BFS搜索时需要某个对象的引用信息
→ 通过索引seek到文件对应位置
→ 读取该对象的字段信息
→ 避免将整个堆加载到内存
2. 优先级队列的BFS
不是简单的BFS,而是考虑引用类型的优先级:
→ 强引用优先于软引用
→ 软引用优先于弱引用
→ 保证找到的路径是"最强"引用链
→ 这才是真正导致泄漏的原因
3. 已知泄漏的过滤
Shark内置了已知的Android Framework泄漏列表:
→ 如某些ROM的InputMethodManager持有Activity引用
→ 这类泄漏不是App的问题
→ Shark自动标记为"Known Library Leak"
→ 帮助开发者聚焦于自身代码的问题
4. 懒加载的对象图(Lazy HeapGraph)
HeapGraph不预先构建完整的对象关系图:
→ 每个HeapObject只在被访问时才解析其字段
→ 使用Sequence(懒序列)遍历字段引用
→ 大幅减少内存占用
→ 适合在移动设备上运行
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 06.方案基础设计
# 6.1 整体架构图
LeakCanary整体架构:
┌─────────────────────────────────────────────┐
│ 应用层 │
│ Activity / Fragment / ViewModel / Service │
└────────────────────┬────────────────────────┘
│ 生命周期回调
┌────────────────────▼────────────────────────┐
│ Watcher层 │
│ ActivityWatcher / FragmentAndViewModelWatcher│
│ ServiceWatcher / RootViewWatcher │
└────────────────────┬────────────────────────┘
│ expectWeaklyReachable()
┌────────────────────▼────────────────────────┐
│ ObjectWatcher │
│ WeakReference + ReferenceQueue │
│ watchedObjects Map / 泄漏判定逻辑 │
└────────────────────┬────────────────────────┘
│ 泄漏确认
┌────────────────────▼────────────────────────┐
│ HeapDumper │
│ Debug.dumpHprofData() / hprof文件生成 │
└────────────────────┬────────────────────────┘
│ hprof文件
┌────────────────────▼────────────────────────┐
│ Shark分析引擎 │
│ HprofParser / HeapGraph / BFS搜索 │
│ 引用链分析 / 泄漏签名 / 分组 │
└────────────────────┬────────────────────────┘
│ 分析结果
┌────────────────────▼────────────────────────┐
│ 展示层 │
│ Logcat / Notification / LeakActivity │
└─────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 5.2 UML设计图
核心类关系:
AppWatcher
├── ObjectWatcher(核心监控器)
│ ├── watchedObjects: Map<String, KeyedWeakReference>
│ ├── queue: ReferenceQueue<Any>
│ └── expectWeaklyReachable() / checkRetainedCount()
│
├── ActivityWatcher(Activity监听器)
├── FragmentAndViewModelWatcher(Fragment+ViewModel监听器)
├── ServiceWatcher(Service监听器)
└── RootViewWatcher(RootView监听器)
HeapAnalyzer
├── SharkHeapGrowthDetector
├── HeapDumpTrigger
├── AndroidDebugHeapDumper
└── HeapAnalyzerService
Shark
├── HprofReader(hprof文件读取器)
├── HprofIndex(内存索引)
├── HeapGraph(对象图)
├── PathFinder(BFS路径查找)
└── LeakTraceObject(引用链节点)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 5.3 关键流程图
关键流程 - 从Activity销毁到报告输出:
Activity.onDestroy()
↓
ActivityWatcher.onActivityDestroyed(activity)
↓
ObjectWatcher.expectWeaklyReachable(activity, desc)
├── 生成UUID作为key
├── 创建KeyedWeakReference(activity, key, desc, queue)
└── watchedObjects[key] = reference
↓
延迟5秒执行 moveToRetained(key)
├── removeWeaklyReachableObjects() → 清理已回收对象
└── watchedObjects中是否还有该key?
├── 没有 → 已回收,正常结束
└── 有 → retainedUptimeMillis设置当前时间 → 通知泄漏
↓
HeapDumpTrigger.checkRetainedObjects()
├── 泄漏计数 < 阈值? → 发通知 + 等待
└── 泄漏计数 >= 阈值 → dumpHeap()
↓
Debug.dumpHprofData(file) → 生成hprof文件
↓
HeapAnalyzerService.analyze(heapDumpFile)
├── Shark解析hprof
├── BFS搜索最短引用链
└── 生成LeakTrace
↓
展示结果(Logcat + Notification + UI页面)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 5.4 接口设计图
核心接口设计:
interface ReachabilityWatcher {
fun expectWeaklyReachable(watchedObject: Any, description: String)
}
interface InstallableWatcher {
fun install()
fun uninstall()
}
interface HeapDumper {
fun dumpHeap(): DumpHeapResult
}
interface OnObjectRetainedListener {
fun onObjectRetained()
}
// ObjectWatcher实现了ReachabilityWatcher
// 各种Watcher实现了InstallableWatcher
// AndroidDebugHeapDumper实现了HeapDumper
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 5.5 模块间依赖关系
模块依赖关系:
leakcanary-android(主模块)
├── leakcanary-android-core(核心逻辑)
│ ├── leakcanary-object-watcher(对象监控)
│ │ └── leakcanary-object-watcher-android(Android平台适配)
│ └── shark(hprof分析引擎)
│ ├── shark-hprof(hprof文件解析)
│ ├── shark-graph(对象图构建)
│ └── shark-android(Android平台适配)
└── leakcanary-android-process(可选,多进程分析)
plumber-android(Android平台修复库)
└── 已知的Android框架泄漏的自动修复
2
3
4
5
6
7
8
9
10
11
12
13
14
# 06.其他设计说明
# 6.1 性能设计优化
为什么LeakCanary不能用于线上?
- 每次内存泄漏都会生成并解析hprof文件,容易引起手机卡顿
- Debug.dumpHprofData()会锁堆(Stop-The-World),应用冻结数秒
- 多次调用GC,可能对线上性能产生影响
- hprof文件较大(10-30MB),信息回捞困难
- Shark分析过程消耗较多内存和CPU
可能的线上优化方案:
- 根据手机信息设定内存阈值M,已使用内存小于M时只记录泄漏信息,不生成hprof
- 当引用链路相同时进行去重
- 不直接回捞hprof文件,选择回捞分析结果
- 将已泄漏对象存储在数据库中,同一泄漏只检测一次
- 使用KOOM的fork+COW方案避免锁堆
# 6.2 稳定性设计
LeakCanary在稳定性方面做了多方面考量:
稳定性设计:
1. 异常兜底
└── 所有Hook操作都有try-catch保护
└── 分析失败不会导致应用崩溃
└── Shark分析异常会输出错误信息而非Crash
2. 内存保护
└── Shark使用按需加载,避免OOM
└── hprof文件分析完毕后及时删除
└── 限制最大保留的hprof文件数量
3. 线程安全
└── ObjectWatcher的watchedObjects使用synchronized保护
└── ReferenceQueue的poll操作是线程安全的
└── 泄漏计数使用原子操作
4. 版本兼容
└── 不同Android版本的Hook策略不同
└── 对反射调用做版本适配
└── 优雅降级:Hook失败时不影响应用正常运行
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 6.3 灰度设计
在实际项目中使用LeakCanary时的灰度策略:
灰度策略建议:
1. Debug包默认开启
└── 开发和测试阶段全量开启
└── 帮助尽早发现泄漏
2. Release包关闭
└── LeakCanary默认只在debug包中生效
└── debugImplementation 'com.squareup.leakcanary:...'
└── Release包不包含LeakCanary代码
3. 线上灰度(使用KOOM等替代方案时)
└── 先灰度1%用户
└── 观察性能影响
└── 逐步扩大灰度比例
└── 设置采样率控制
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 6.4 降级设计
降级策略:
1. 设备性能降级
└── 低端设备(RAM < 3GB)可以关闭检测
└── 减少监控的组件类型(只监控Activity)
2. 阈值降级
└── 提高泄漏对象触发Dump的阈值(从5提高到10)
└── 增加两次Dump的最小间隔(从60s提高到120s)
3. 分析降级
└── 在主进程内存紧张时推迟分析
└── 使用独立进程分析,不影响主进程
2
3
4
5
6
7
8
9
10
11
12
13
# 6.5 异常设计优化
异常处理策略:
1. Hook失败处理
└── 反射Hook失败时优雅降级
└── 不影响应用正常功能
└── 输出日志供开发者排查
2. Dump失败处理
└── Debug.dumpHprofData()可能抛出IOException
└── 磁盘空间不足时的处理
└── 失败后重新调度下一次Dump
3. 分析失败处理
└── hprof文件损坏的处理
└── 分析超时的处理
└── 内存不足时的处理(中断分析)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
关于finalize方法的补充说明:
LeakCanary在触发GC后还调用了System.runFinalization(),这是强制调用已失去引用对象的finalize方法。在可达性算法中,不可达对象也不是立即死亡的,需要经过两次标记过程。第一次标记后会筛选是否需要执行finalize()方法,如果对象没有覆盖finalize()或者finalize()已经被调用过,则视为"没有必要执行",直接回收。调用runFinalization()可以加速这个过程,让判断更加准确。
# 07.线上内存泄漏检测方案
# 7.1 LeakCanary为何不能用于线上
核心原因总结:
LeakCanary不适合线上的原因:
性能影响:
├── GC触发:主动调用GC会导致应用短暂卡顿
├── 堆锁定:dumpHprofData()会Stop-The-World,冻结数秒
├── 分析耗时:Shark分析过程消耗CPU和内存
└── IO开销:hprof文件10-30MB,磁盘读写量大
用户体验:
├── 应用冻结导致用户感知卡顿
├── 前台通知干扰用户
└── 分析过程影响应用流畅性
资源消耗:
├── hprof文件占用大量磁盘空间
├── 分析过程可能导致内存峰值
└── 频繁GC影响内存分配效率
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.2 KOOM线上检测方案
快手KOOM(Kwai OOM)的核心创新是利用Linux的fork+COW机制实现无冻结的HeapDump:
KOOM工作流程:
1. 监控阶段
└── 定期采样Java堆内存使用量
└── 当内存使用量连续N次超过阈值时触发检测
└── 比LeakCanary的对象级监控更轻量
2. Dump阶段(核心优化)
└── 调用fork()创建子进程
└── 子进程继承父进程的完整内存快照(COW)
└── 父进程立即恢复执行(不受影响)
└── 子进程中执行Debug.dumpHprofData()
└── 子进程完成后自动退出
3. 分析阶段
└── 在子进程或后台Service中分析hprof
└── 使用裁剪后的hprof(只保留必要信息)
└── 生成轻量级的分析报告
4. 上报阶段
└── 只上报分析结果(JSON格式)
└── 不上报hprof文件
└── 通过网络上报到后端
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 7.3 线上内存泄漏监控体系
完整的线上内存监控体系:
采集层:
├── Java堆内存采样
├── Native内存采样
├── 大对象监控(>1MB的Bitmap等)
├── Activity/Fragment泄漏检测
└── 内存触顶告警
分析层:
├── KOOM离线分析
├── hprof裁剪与压缩
├── 引用链提取
└── 泄漏自动归类
上报层:
├── 采样率控制(按设备、版本、用户分层)
├── 流量控制(非WiFi环境不上报大文件)
├── 去重机制(相同泄漏只上报一次)
└── 数据脱敏
展示层:
├── 泄漏大盘(泄漏率趋势、Top N泄漏)
├── 版本对比(新版本是否引入新泄漏)
├── 详情页面(引用链、影响范围)
└── 告警通知(新泄漏自动告警)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 7.4 各方案对比分析
内存泄漏检测方案对比:
| 特性 | LeakCanary | KOOM | Matrix |
|---------------|--------------|--------------|---------------|
| 适用环境 | 开发/测试 | 线上 | 线上 |
| 检测方式 | 对象级监控 | 内存阈值触发 | 对象级+阈值 |
| Dump方式 | 主进程直接Dump | fork子进程 | fork子进程 |
| 应用冻结 | 数秒冻结 | 无冻结 | 无冻结 |
| 分析引擎 | Shark | 自研 | 自研 |
| 性能影响 | 较大 | 小 | 小 |
| 接入成本 | 零配置 | 需要配置 | 需要配置 |
| 开源 | 是 | 是 | 是 |
2
3
4
5
6
7
8
9
10
11
12
# 08.常见面试题深度解析
# 8.1 经典面试问题
问题1:LeakCanary的工作原理是什么?
回答要点:
1. 自动初始化:通过ContentProvider在Application创建前完成初始化
2. 生命周期监听:注册全局监听器/Hook,感知五种场景的组件销毁时机
3. 泄漏检测:
使用WeakReference + ReferenceQueue机制
对象销毁后创建WeakReference关联ReferenceQueue
5秒后检查引用是否进入队列
未进入则触发GC → 再次检查 → 仍未进入则确认泄漏
4. HeapDump:
泄漏计数达到阈值后调用Debug.dumpHprofData()
生成hprof内存快照文件
5. 分析报告:
Shark引擎分析hprof文件
BFS搜索从泄漏对象到GC Root的最短引用链
输出引用链报告
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
问题2:WeakReference和ReferenceQueue的关系?
WeakReference指向的对象被GC回收时:
1. GC线程发现对象只有弱引用(无强引用/软引用)
2. 回收该对象
3. 将WeakReference放入关联的ReferenceQueue
4. 应用线程通过queue.poll()获取已入队的引用
LeakCanary利用此机制:
创建WeakReference(obj, queue) → 等待GC
→ poll queue → 弱引用在队列中 → 对象已回收(正常)
→ poll queue → 弱引用不在队列中 → 对象未回收(泄漏)
2
3
4
5
6
7
8
9
10
问题3:LeakCanary如何做到零配置?
通过ContentProvider自动初始化:
1. 在AAR的AndroidManifest中注册AppWatcherInstaller(ContentProvider)
2. Android系统在创建Application时自动创建此ContentProvider
3. ContentProvider.onCreate()在Application.onCreate()之前调用
4. 在onCreate()中完成LeakCanary的所有初始化工作
5. 开发者只需添加依赖,无需任何初始化代码
2
3
4
5
6
# 8.2 进阶面试问题
问题4:LeakCanary为什么不能用于线上?KOOM是如何解决的?
LeakCanary不能用于线上的原因:
1. dumpHprofData()会锁堆,应用冻结数秒
2. 频繁GC影响性能
3. hprof文件大,占用磁盘和IO
KOOM的解决方案:
1. 使用fork()创建子进程
2. 子进程通过COW获得父进程内存快照
3. 父进程立即恢复,不受影响
4. 子进程中执行Dump和分析
5. 只上报分析结果,不上报hprof文件
2
3
4
5
6
7
8
9
10
11
问题5:LeakCanary如何监控ViewModel的销毁?
由于Android Framework未提供ViewModel.onCleared()的全局监听:
1. 在Activity/Fragment的onCreate中
通过ViewModelProvider创建一个自定义ViewModelClearedWatcher
2. 当Activity/Fragment销毁时
ViewModelClearedWatcher.onCleared()被调用
3. 在onCleared()中
通过反射获取ViewModelStore内部的map
遍历所有ViewModel对象
将它们交给ObjectWatcher监控
2
3
4
5
6
7
8
9
10
11
12
问题6:Shark相比haha有什么优势?
Shark优势:
1. 按需加载:不一次性加载整个hprof到内存,使用索引+seek方式
2. 两次遍历:第一次建索引,第二次按需读取,减少IO操作
3. 使用Okio:高效的IO库,减少内存拷贝
4. Kotlin编写:更安全的空安全处理
5. 分析速度:比haha快约6倍
6. 内存消耗:远低于haha
2
3
4
5
6
7
# 8.3 学习建议
学习LeakCanary建议按以下顺序:
- 理解基础概念:Java引用类型、WeakReference、ReferenceQueue
- 掌握核心原理:WeakReference + ReferenceQueue的泄漏检测机制
- 学习初始化:ContentProvider自动初始化的设计思想
- 深入组件监听:各种Watcher如何Hook不同组件的生命周期
- 了解分析引擎:Shark如何解析hprof文件并查找引用链
- 扩展学习:KOOM等线上方案的fork+COW优化思路
# 参考博客
- 为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为是真强啊!
- https://zhuanlan.zhihu.com/p/556718460
- 一文让你彻底理解LeakCanary的工作原理
- https://mp.weixin.qq.com/s/UfxG41HInNfv9nkDvKpcZQ
- 「Leakcanary 源码分析」看这一篇就够了
- https://mp.weixin.qq.com/s/n3_Zoc1UgG3Wzqv3ZrGMzA