编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • 库的解读

      • README
      • 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 整体架构图
          • 5.2 UML设计图
          • 5.3 关键流程图
          • 5.4 接口设计图
          • 5.5 模块间依赖关系
        • 06.其他设计说明
          • 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 学习建议
          • 参考博客
      • Glide图片加载设计
      • OkHttp网络框架设计
      • EventBus事件总设计
      • ARouter路由实践设计
    • 专栏博客

    • 智能硬件

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • Apps
  • Android提升进阶
  • 库的解读
杨充
2025-02-20
目录

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展示
1
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引用,销毁时需特殊处理
1
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场景中的内存泄漏监测:

  1. 已销毁的Activity对象(进入DESTROYED状态)
  2. 已销毁的Fragment对象和Fragment View对象(进入DESTROYED状态)
  3. 已清除的ViewModel对象(进入CLEARED状态)
  4. 已销毁的Service对象(进入DESTROYED状态)
  5. 已从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打印分析结果
  └── 发送系统通知
  └── 可视化报告页面展示
1
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()
1
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
    }
}
1
2
3
4
5
6
7
8
9
10
11

初始化做了什么:

  1. 注册Activity生命周期监听(通过registerActivityLifecycleCallbacks)
  2. 注册Fragment生命周期监听
  3. 注册ViewModel清除监听
  4. 注册Service销毁监听(通过Hook)
  5. 注册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, ...)
     }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Service监控:

  • Android Framework未提供Service.onDestroy()全局监听方法
  • 需要两步Hook来实现:
    1. Hook主线程消息循环的mH.mCallback回调,监听STOP_SERVICE消息,暂存即将销毁的Service对象
    2. 使用动态代理Hook IActivityManager Binder对象,代理serviceDoneExecuting()方法,视为Service.onDestroy()的执行时机

RootView监控:

  • 通过Hook WindowManagerGlobal.mViews RootView列表获取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
1
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)
    }
}
1
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事件
  ↓
开启前台服务进行分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Dump的两种触发策略:

  1. 自动触发:泄漏对象计数达到阈值(默认5个)时自动触发
  2. 手动触发:用户点击通知栏提前触发分析

# 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. 生成分析报告
   └── 输出引用链
   └── 标记怀疑对象
   └── 分组相同泄漏
1
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倍?

答疑:

  1. haha将整个hprof文件加载到内存,内存消耗巨大;Shark使用索引+按需加载,内存消耗小得多
  2. haha使用Java的反射机制解析;Shark使用高效的Okio进行IO操作
  3. Shark对hprof文件只做两次遍历(建索引+分析),而haha需要多次遍历
  4. 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到泄漏对象
1
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本身非常快(只复制页表)
  → 子进程获得了父进程内存的完整快照
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

KOOM还做了以下优化:

  1. 使用内存阈值触发检测(而非对象计数)
  2. 优化hprof文件裁剪,减小文件体积
  3. 支持线上环境使用

# 03.LeakCanary原理

# 3.1 思考一些问题

在深入源码之前,先思考几个问题:

核心问题:
1. LeakCanary的核心工作流程是什么?设计思想是什么?
2. 初始化做了哪些工作?如何做到零配置?
3. 如何监听各种组件的销毁事件?尤其是ViewModel、Service?
4. 如何判断对象是否泄漏?判定条件是什么?
5. WeakReference + ReferenceQueue的检测机制原理?
6. 检测出泄漏后如何生成泄漏信息?引用链如何得到?
7. 为什么不能用于线上?有什么替代方案?
1
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触发后弱引用还没有被清理,则认为可能发生内存泄漏。

判断是否存在内存泄漏:

  1. 尝试从ReferenceQueue中获取待分析对象对应的弱引用
  2. 如果弱引用已在队列中,说明对象正在被回收 → 返回DONE
  3. 如果不在队列中,可能存在泄漏 → 手动触发GC
  4. GC后再次检查引用队列
  5. 如果仍不在队列中 → 确认泄漏

分析内存泄漏:

确认泄漏后,调用heapDumper.dumpHeap()生成hprof文件,Shark引擎分析hprof文件查找从泄漏对象到GC Root的最短引用链。

整体流程图:

组件销毁(onDestroy等)
  ↓
ObjectWatcher.expectWeaklyReachable(object)
  ↓
创建KeyedWeakReference + ReferenceQueue
  ↓
等待5秒
  ↓
检查ReferenceQueue
  ├── 弱引用在队列中 → 对象已回收 → 移除监控(正常)
  └── 弱引用不在队列中 → 触发GC → 再次检查
       ├── 在队列中 → 移除监控(正常)
       └── 不在队列中 → 确认泄漏 → 泄漏计数+1
            ↓
       泄漏计数 >= 阈值?
            ├── 否 → 发送通知提醒
            └── 是 → HeapDump → Shark分析 → 输出报告
1
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)
    )
}
1
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)
    }
}
1
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)
}
1
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()"
            )
        }
    }
}
1
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监控
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

判断逻辑详细说明:

  1. 对要监听的对象,使用KeyedWeakReference与其关联(初始化时传入引用队列queue),并保存到watchedObjects Map中
  2. 使用Handler延迟5秒后执行判断
  3. 判断时先遍历ReferenceQueue,将已回收对象从watchedObjects中移除
  4. 再检查目标对象是否仍在watchedObjects中
  5. 如果仍在,调用Runtime.getRuntime().gc()触发GC
  6. GC后等待100ms,再次执行步骤3-4
  7. 如果对象仍然存在 → 确认泄漏

# 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)
}
1
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. 开启前台服务执行分析工作
1
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
  └── 计算泄漏签名
  └── 分组相同泄漏
1
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通过多种方式输出结果:

  1. Logcat输出:在控制台打印完整的引用链信息
  2. 系统通知:发送通知栏消息,点击可跳转到详情页
  3. 可视化页面:LeakCanary提供了专门的Activity展示泄漏信息
  4. 桌面快捷方式:生成快捷入口方便查看

# 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
1
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中 → 对象未回收(可能泄漏)
1
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,保证是最短路径
1
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到文件对应位置读取
  → 避免将整个堆加载到内存
1
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。

优化方案:

  1. 多进程分析:将分析工作放到独立进程中,不影响主进程性能
  2. 线程异步执行:即使在同一进程中,也使用后台线程执行
  3. 按需加载:Shark引擎不一次性加载整个hprof到内存
  4. 缓存机制:对相同泄漏模式的结果进行缓存
分析策略选择:

LeakCanary会根据依赖项选择策略:
├── 如果添加了leakcanary-android-process依赖
│   └── 使用独立进程(:leakcanary)进行分析
│   └── 完全不影响主进程
├── 否则
│   └── 使用后台Thread进行分析
│   └── 开启前台Service保证不被杀
1
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")

签名相同 → 归为同一组 → 只需排查一次
1
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
1
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等复杂组件
   → 降低使用者的学习成本
1
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修饰符防止外部继承和使用
1
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更灵活,每个子类可以有不同字段
// → 比普通继承更安全,限制了子类范围
1
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可以用于任何接口
1
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不受系统时间修改影响
//    避免用户手动改时间导致判断逻辑出错
1
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. 这是整洁架构中"依赖倒置原则"的体现
1
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(懒序列)遍历字段引用
   → 大幅减少内存占用
   → 适合在移动设备上运行
1
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       │
└─────────────────────────────────────────────┘
1
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(引用链节点)
1
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页面)
1
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
1
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框架泄漏的自动修复
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 06.其他设计说明

# 6.1 性能设计优化

为什么LeakCanary不能用于线上?

  1. 每次内存泄漏都会生成并解析hprof文件,容易引起手机卡顿
  2. Debug.dumpHprofData()会锁堆(Stop-The-World),应用冻结数秒
  3. 多次调用GC,可能对线上性能产生影响
  4. hprof文件较大(10-30MB),信息回捞困难
  5. Shark分析过程消耗较多内存和CPU

可能的线上优化方案:

  1. 根据手机信息设定内存阈值M,已使用内存小于M时只记录泄漏信息,不生成hprof
  2. 当引用链路相同时进行去重
  3. 不直接回捞hprof文件,选择回捞分析结果
  4. 将已泄漏对象存储在数据库中,同一泄漏只检测一次
  5. 使用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失败时不影响应用正常运行
1
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%用户
   └── 观察性能影响
   └── 逐步扩大灰度比例
   └── 设置采样率控制
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. 分析降级
   └── 在主进程内存紧张时推迟分析
   └── 使用独立进程分析,不影响主进程
1
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文件损坏的处理
   └── 分析超时的处理
   └── 内存不足时的处理(中断分析)
1
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影响内存分配效率
1
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文件
   └── 通过网络上报到后端
1
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泄漏)
├── 版本对比(新版本是否引入新泄漏)
├── 详情页面(引用链、影响范围)
└── 告警通知(新泄漏自动告警)
1
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         | 自研          | 自研          |
| 性能影响       | 较大          | 小            | 小            |
| 接入成本       | 零配置        | 需要配置      | 需要配置      |
| 开源           | 是            | 是            | 是            |
1
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的最短引用链
   输出引用链报告
1
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 → 弱引用不在队列中 → 对象未回收(泄漏)
1
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. 开发者只需添加依赖,无需任何初始化代码
1
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文件
1
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监控
1
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
1
2
3
4
5
6
7

# 8.3 学习建议

学习LeakCanary建议按以下顺序:

  1. 理解基础概念:Java引用类型、WeakReference、ReferenceQueue
  2. 掌握核心原理:WeakReference + ReferenceQueue的泄漏检测机制
  3. 学习初始化:ContentProvider自动初始化的设计思想
  4. 深入组件监听:各种Watcher如何Hook不同组件的生命周期
  5. 了解分析引擎:Shark如何解析hprof文件并查找引用链
  6. 扩展学习: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
上次更新: 2026/06/10, 11:13:41
README
Glide图片加载设计

← README Glide图片加载设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式