编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • Kotlin精通

    • 库的解读

      • 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 输出分析报告
          • 3.10 ObjectWatcher内部状态转换完整分析
        • 04.一些技术点思考
          • 4.1 WeakReference与ReferenceQueue机制
          • 4.2 引用链如何生成
          • 4.3 Shark分析引擎原理
          • 4.4 提高Dump分析效率
          • 4.5 相同问题分组
          • 4.6 如何标记怀疑对象
          • 4.7 GC触发与等待策略深度解析
          • 4.8 堆快照解析的两次遍历策略深度解析
        • 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.其他设计说明
          • 7.1 性能设计优化
          • 7.2 稳定性设计
          • 7.3 灰度设计
          • 7.4 降级设计
          • 7.5 异常设计优化
        • 08.线上内存泄漏检测方案
          • 8.1 LeakCanary为何不能用于线上
          • 8.2 KOOM线上检测方案
          • 8.3 线上内存泄漏监控体系
          • 8.4 各方案对比分析
        • 09.常见面试题深度解析
          • 9.1 经典面试问题
          • 9.2 进阶面试问题
          • 9.3 学习建议
      • Glide图片加载设计
      • OkHttp网络框架设计
      • EventBus事件总设计
      • ARouter路由实践设计
    • 专栏博客

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • IoT智能硬件开发

  • 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 输出分析报告
    • 3.10 ObjectWatcher内部状态转换完整分析
  • 04.一些技术点思考
    • 4.1 WeakReference与ReferenceQueue机制
    • 4.2 引用链如何生成
    • 4.3 Shark分析引擎原理
    • 4.4 提高Dump分析效率
    • 4.5 相同问题分组
    • 4.6 如何标记怀疑对象
    • 4.7 GC触发与等待策略深度解析
    • 4.8 堆快照解析的两次遍历策略深度解析
  • 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.其他设计说明
    • 7.1 性能设计优化
    • 7.2 稳定性设计
    • 7.3 灰度设计
    • 7.4 降级设计
    • 7.5 异常设计优化
  • 08.线上内存泄漏检测方案
    • 8.1 LeakCanary为何不能用于线上
    • 8.2 KOOM线上检测方案
    • 8.3 线上内存泄漏监控体系
    • 8.4 各方案对比分析
  • 09.常见面试题深度解析
    • 9.1 经典面试问题
    • 9.2 进阶面试问题
    • 9.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 内存泄漏概念

什么是内存泄漏? 当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.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打印分析结果
  └── 发送系统通知
  └── 可视化报告页面展示

# 2.2 初始化思路

LeakCanary 2.x利用ContentProvider实现零配置自动初始化,这是一个非常巧妙的设计。

疑惑:为什么不需要在Application中手动初始化? ContentProvider的onCreate()在Application.onCreate()之前调用。Android应用启动时组件初始化顺序:

Application.attachBaseContext()
  ↓
ContentProvider.onCreate()   ← LeakCanary在这里初始化
  ↓
Application.onCreate()
  ↓
Activity.onCreate()

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. 注册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, ...)
     }

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

利用这个特性,可以检测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)
    }
}

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事件
  ↓
开启前台服务进行分析

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

疑惑:为什么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到泄漏对象

# 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本身非常快(只复制页表)
  → 子进程获得了父进程内存的完整快照

KOOM还做了以下优化:

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

# 03.LeakCanary原理

# 3.1 思考一些问题

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

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

原理简单介绍:

通过监听组件的生命周期(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分析 → 输出报告

# 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)
    )
}

# 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)
    }
}

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)
}

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()"
            )
        }
    }
}

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监控

# 3.5 监听无用对象

ObjectWatcher是LeakCanary的核心组件,负责管理所有被监控对象的生命周期:

ObjectWatcher内部数据结构:

watchedObjects: LinkedHashMap<String, KeyedWeakReference>
  ├── key: UUID随机生成的唯一标识
  └── value: KeyedWeakReference(继承WeakReference)
  └── 使用LinkedHashMap保持插入顺序,便于遍历

queue: ReferenceQueue<Any>
  └── 当WeakReference指向的对象被回收时
  └── 该WeakReference会自动进入此队列

retainedObjects: MutableSet<String>
  └── 存储已确认泄漏的对象的key
  └── 每次dump后清空并重新累积

KeyedWeakReference:
  ├── key: String(唯一标识)
  ├── description: String(描述信息)
  ├── watchUptimeMillis: Long(开始监控的时间)
  ├── retainedUptimeMillis: Long(确认泄漏的时间,-1表示未泄漏)
  └── 继承自WeakReference<Any>,关联ReferenceQueue

判断逻辑详细说明:

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

完整源码级流程分析:

// ObjectWatcher.expectWeaklyReachable() 完整实现
@Synchronized
fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
) {
    if (!isEnabled()) return  // 功能开关检查
    
    // 第一步:先清理已经入队的引用(已回收的对象)
    removeWeaklyReachableObjects()
    
    // 第二步:生成唯一key并创建KeyedWeakReference
    val key = UUID.randomUUID().toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference = KeyedWeakReference(
        watchedObject, key, description, watchUptimeMillis, queue
    )
    
    SharkLog.d {
        "Watching " +
        (if (watchedObject is Class<*>) watchedObject.toString() 
         else "instance of ${watchedObject.javaClass.name}") +
        (if (description.isNotEmpty()) " ($description)" else "") +
        " with key $key"
    }
    
    // 第三步:存入监控Map
    watchedObjects[key] = reference
    
    // 第四步:延迟5秒后检查(核心异步检查逻辑)
    checkRetainedExecutor.execute {
        moveToRetained(key)
    }
}

removeWeaklyReachableObjects() - 清理已回收对象的关键方法:

private fun removeWeaklyReachableObjects() {
    // ReferenceQueue.poll() 非阻塞获取已入队的引用
    // 如果该引用在队列中 → 说明对象已被GC回收 → 从watchedObjects中安全移除
    var ref: KeyedWeakReference?
    do {
        ref = queue.poll() as KeyedWeakReference?
        if (ref != null) {
            watchedObjects.remove(ref.key)
        }
    } while (ref != null)
}

moveToRetained() - 判定泄漏的核心方法:

@Synchronized
private fun moveToRetained(key: String) {
    // 再次清理已回收对象(5秒内可能又有GC发生)
    removeWeaklyReachableObjects()
    
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {
        // 对象仍在监控Map中 → 疑似泄漏
        retainedRef.retainedUptimeMillis = clock.uptimeMillis()
        
        // 检查是否已被用户忽略(通过UI手动忽略)
        if (!ignoredObjectMatcher.isIgnored(retainedRef)) {
            retainedObjects.add(key)  // 加入已确认泄漏集合
        }
        
        // 通知所有泄漏监听器
        onObjectRetainedListeners.forEach { 
            it.onObjectRetained() 
        }
        
        // 调度下一次检查(HeapDumpTrigger会响应)
        checkRetainedExecutor.execute {
            checkRetainedObjects()
        }
    }
}

GC触发策略的深入分析:

LeakCanary在5秒等待后,如果对象未被回收,不会立即判定为泄漏,而是执行一套GC重试策略:

// GC触发策略(在HeapDumpTrigger中)
fun checkRetainedObjects(reason: String) {
    val config = configProvider()
    
    // 第一次检查:只看引用队列
    var retainedReferenceCount = objectWatcher.retainedObjectCount
    if (retainedReferenceCount == 0) return
    
    // 第二次:手动触发GC,再次检查
    // 一次GC可能不够,Java的GC不保证完全回收
    var gcTriggerCount = 0
    while (gcTriggerCount < 3 && retainedReferenceCount > 0) {
        Runtime.getRuntime().gc()
        // 等待100ms让GC完成
        Thread.sleep(100)
        // 运行finalization(处理有finalize()方法的对象)
        Runtime.getRuntime().runFinalization()
        
        // 重新检查引用队列
        objectWatcher.removeWeaklyReachableObjects()
        retainedReferenceCount = objectWatcher.retainedObjectCount
        gcTriggerCount++
    }
    
    // 三次GC后仍存在 → 高度确信发生泄漏
    if (retainedReferenceCount > 0) {
        // 判断是否需要dump
        // ...
    }
}

为什么需要多次GC?

Java的GC并不是请求就立即执行且保证完全回收的:

  • System.gc() / Runtime.gc() 只是一个建议,JVM不一定立即响应
  • 即使GC执行了,一次GC可能只回收部分代(如Young GC不回收Old Gen)
  • 有finalize()方法的对象需要两次GC:第一次标记后执行finalize,第二次才能真正回收
  • 调用runFinalization()可以加速finalize方法的执行,让二次标记更快完成

# 3.6 监控内存泄漏

LeakCanary确认泄漏后的处理由一个完整的状态机驱动,核心类为HeapDumpTrigger:

HeapDumpTrigger 内部状态机:

HeapDumpTrigger 五种状态:

IDLE(空闲)
  └── 初始状态 / Dump完成后的状态
  └── 等待新的泄漏事件

WAITING_FOR_RETAINED_COUNT(等待泄漏累积)
  └── 已发现泄漏对象,但未达阈值
  └── 显示通知,提示有N个泄漏对象
  └── 用户可手动点击通知触发dump

WAITING_FOR_DUMP_INTERVAL(等待dump冷却)
  └── 泄漏已达阈值,但距离上次dump不足60秒
  └── 等待冷却时间结束

DUMPING(正在Dump)
  └── 正在执行Debug.dumpHprofData()
  └── 应用冻结状态

ANALYZING(正在分析)
  └── Shark引擎正在分析hprof文件
  └── 在后台线程/独立进程中执行

完整泄漏处理决策流程:

// HeapDumpTrigger 核心调度逻辑
fun checkRetainedObjects() {
    val retainedKeys = objectWatcher.retainedObjects.toList()
    val count = retainedKeys.size
    
    if (count == 0) {
        // 无泄漏,回到IDLE状态
        return
    }
    
    // ====== 决策1: 冷却期检查 ======
    val lastDumpTime = lastDisplayedRetainedObjectCount?.lastHeapDumpTime ?: 0
    val timeSinceLastDump = SystemClock.uptimeMillis() - lastDumpTime
    if (timeSinceLastDump < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
        // 距离上次dump不足60秒,等待冷却
        // 状态: WAITING_FOR_DUMP_INTERVAL
        scheduleRetainedCheck(
            delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - timeSinceLastDump
        )
        return
    }
    
    // ====== 决策2: 阈值检查 ======
    val minRetainedCount = if (applicationVisible) {
        retainedVisibleThreshold  // 前台阈值,默认5
    } else {
        retainedInvisibleThreshold  // 后台阈值,默认较小
    }
    
    if (count < minRetainedCount) {
        // 泄漏数量未达标,发送通知但不dump
        // 状态: WAITING_FOR_RETAINED_COUNT
        updateRetainedCountNotification(count)
        scheduleRetainedCheck(retainedDelayMillis)
        return
    }
    
    // ====== 决策3: 后台策略 ======
    if (!applicationVisible) {
        // App在后台,可以更宽松地触发dump
        // 后台dump对用户体验影响小
    }
    
    // ====== 决策4: 执行Dump ======
    // 状态: DUMPING
    dumpHeap(retainedKeys)
}

泄漏对象的生命周期管理:

// ObjectWatcher中泄漏对象的管理
class ObjectWatcher {
    // watchedObjects: 所有正在监控的对象(包括已确认泄漏的)
    private val watchedObjects = LinkedHashMap<String, KeyedWeakReference>()
    
    // retainedObjects: 已确认泄漏的对象的key集合
    private val retainedObjects = MutableSet<String>()
    
    val retainedObjectCount: Int
        get() = retainedObjects.size
    
    // 移除已确认泄漏的对象(dump完成后调用)
    fun removeRetainedObjects() {
        synchronized(this) {
            retainedObjects.clear()
        }
    }
}

前后台感知策略:

// LeakCanary通过ProcessLifecycleOwner感知前后台
// 前台泄漏阈值更高(减少对用户的干扰)
// 后台泄漏阈值更低(更积极地检测)

// 前台阈值 vs 后台阈值
config = config.copy(
    retainedVisibleThreshold = 5,     // 前台:积累5个泄漏才dump
    retainedInvisibleThreshold = 1,   // 后台:1个泄漏即可dump
    dumpHeapWhenDebugging = false,    // Debug时不dump(避免干扰调试)
)

两种触发Dump的路径:

  1. 自动触发:泄漏对象计数达到阈值 → 自动触发dump
  2. 手动触发:用户看到通知栏的"X个泄漏对象",点击通知 → 手动触发dump(绕过阈值检查)

# 3.7 Dump内存快照

在判定有内存泄漏后,LeakCanary调用系统提供的Debug.dumpHprofData(File)函数,生成虚拟机的内存快照(hprof文件)。

HPROF文件格式简介:

HPROF(Heap/CPU Profiling Tool)是JVM标准的堆转储格式,Android ART/Dalvik虚拟机采用类似的格式。hprof文件本质上是一个二进制文件,由一系列Record串联组成:

HPROF文件结构:

┌──────────────────────────────┐
│  Header(文件头)              │
│  ├── format string            │
│  ├── 标识符大小(4或8字节)     │
│  └── 时间戳                   │
├──────────────────────────────┤
│  Record 1(字符串表)          │
├──────────────────────────────┤
│  Record 2(类定义)            │
├──────────────────────────────┤
│  Record 3(实例数据)          │
│  ├── 对象ID                   │
│  ├── 堆栈跟踪序列号            │
│  ├── 类ID                     │
│  └── 实例字段值(基本类型+引用)│
├──────────────────────────────┤
│  Record 4(数组数据)          │
│  ├── 数组对象ID               │
│  ├── 堆栈跟踪序列号            │
│  ├── 元素数量                 │
│  ├── 元素类型                 │
│  └── 元素值                   │
├──────────────────────────────┤
│  Record 5(GC Root)          │
│  ├── JNI Global/Local         │
│  ├── Java Frame               │
│  ├── Static Field             │
│  ├── Thread Object            │
│  ├── System Class             │
│  ├── Monitor Used             │
│  └── Sticky Class             │
├──────────────────────────────┤
│  ...更多Record...              │
└──────────────────────────────┘

Dump实现细节:

Dump流程:

1. 触发条件满足 → 开始Dump

2. 调用 AndroidDebugHeapDumper.dumpHeap()
   └── 内部调用 Debug.dumpHprofData(file.getAbsolutePath())
   └── 该方法会暂停所有线程(Stop-The-World)
   └── GC线程遍历Java堆中的所有可达对象
   └── 将对象信息(实例数据、数组、类信息)序列化为HPROF格式
   └── 写入目标文件
   └── 恢复所有线程

3. Stop-The-World 详解:
   └── dumpHprofData内部调用 VMDebug.dumpHprofData()
   └── 这是一个native方法,最终调用ART的Heap::Dump()
   └── 需要先获取mutator_lock(全局互斥锁)
   └── 暂停所有Java线程 → 冻结时间取决于堆大小
   └── 堆100MB时,冻结约1-2秒
   └── 堆300MB时,冻结约3-5秒

4. 生成hprof文件(通常10-30MB)
   └── 文件大小 ≈ 堆中对象的总内存占据量
   └── 包含所有GC Root、类定义、实例数据、数组

5. 将泄漏对象的referenceKey和hprof文件封装为HeapDump对象
   └── HeapDump(hprofFile, referenceKey, excludedRefs, ...)

6. 发送LeakCanary内部事件 → EventBus通知
   └── HeapDumped事件 → 触发分析流程

7. 开启前台服务(HeapAnalyzerService)执行分析工作
   └── 前台服务避免进程被系统kill
   └── 独立线程中执行Shark分析

AndroidDebugHeapDumper源码分析:

// Android平台Dump实现
class AndroidDebugHeapDumper : HeapDumper {
    override fun dumpHeap(): DumpHeapResult {
        val heapDumpFile = createTempFile()
        
        return try {
            // 核心调用:锁堆并生成hprof
            Debug.dumpHprofData(heapDumpFile.absolutePath)
            
            // 验证文件生成成功
            if (heapDumpFile.exists()) {
                DumpHeapResult.Success(heapDumpFile)
            } else {
                DumpHeapResult.FileNotCreated("hprof文件未生成")
            }
        } catch (e: Exception) {
            SharkLog.d(e) { "Heap dump failed" }
            DumpHeapResult.DumpFailed(e.message ?: "Unknown error")
        }
    }
}

Dump过程中的安全保护:

// LeakCanary的Dump保护机制
fun dumpHeap(retainedKeys: List<String>) {
    // 保护1: 标记dump状态,防止重复dump
    if (isDumping) return
    isDumping = true
    
    // 保护2: 记录dump时间,用于冷却期判断
    lastHeapDumpTime = SystemClock.uptimeMillis()
    
    // 保护3: 在dump前最后清理一次引用队列
    // (对象可能在dump前被回收,降低误报)
    objectWatcher.removeWeaklyReachableObjects()
    
    // 保护4: 文件路径冲突处理
    // 旧的hprof文件在分析完成后会被删除
    val heapDumpFile = File(
        context.cacheDir, 
        "leakcanary-${System.currentTimeMillis()}.hprof"
    )
    
    // 执行dump
    val result = heapDumper.dumpHeap()
    
    // 保护5: dump结果校验
    when (result) {
        is DumpHeapResult.Success -> {
            // 发送到分析服务
            HeapAnalyzerService.runAnalysis(context, result.file)
        }
        is DumpHeapResult.FileNotCreated,
        is DumpHeapResult.DumpFailed -> {
            // 重置状态,允许重试
            isDumping = false
        }
    }
}

# 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
  └── 计算泄漏签名
  └── 分组相同泄漏

# 3.9 输出分析报告

分析完成后,LeakCanary通过多种方式输出结果:

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

# 3.10 ObjectWatcher内部状态转换完整分析

ObjectWatcher对每个被监控对象的生命周期管理可以抽象为一个状态机:

单个被监控对象的状态转换:

    expectWeaklyReachable(obj)
            │
            ▼
    ┌──────────────┐
    │   WATCHING   │  等待5秒
    │  (监控中)     │  obj有WeakReference指向
    └──────┬───────┘
           │ 5秒后 checkRetainedExecutor 执行
           ▼
    ┌──────────────┐
    │  CHECKING    │  第一次检查
    │  (检查中)     │  遍历ReferenceQueue
    └──┬────────┬──┘
       │        │
   ref在queue中  ref不在queue中
       │        │
       ▼        ▼
   ┌──────┐  ┌──────────────────┐
   │ DONE │  │  TRIGGER_GC      │  手动触发GC
   │(正常)│  │  (触发GC重试)     │  Runtime.gc()
   └──────┘  └────────┬─────────┘
                      │ GC后100ms再次检查
                      ▼
               ┌──────────┬──────────┐
               │                   │
          ref在queue中         ref不在queue中
               │                   │
               ▼                   ▼
           ┌──────┐        ┌──────────────┐
           │ DONE │        │   RETAINED   │  确认泄漏
           │(正常)│        │  (已泄漏)     │  retained=true
           └──────┘        └──────┬───────┘
                                  │ 通知listener
                                  ▼
                          ┌──────────────┐
                          │  COUNTING    │  累积计数
                          │  (累积中)     │  retainedCount++
                          └──────┬───────┘
                                 │ count >= threshold
                                 ▼
                          ┌──────────────┐
                          │  DUMPING     │  生成hprof
                          │  (Dump中)    │  Debug.dumpHprofData()
                          └──────┬───────┘
                                 │
                                 ▼
                          ┌──────────────┐
                          │  ANALYZING   │  Shark分析
                          │  (分析中)     │  BFS查找引用链
                          └──────┬───────┘
                                 │
                                 ▼
                          ┌──────────────┐
                          │  REPORTED    │  输出报告
                          │  (已报告)     │  Logcat/通知/UI
                          └──────────────┘

ObjectWatcher全局状态管理:

ObjectWatcher内部有一个全局的"是否已通知过泄漏"标志位,用于控制通知频率:

class ObjectWatcher {
    // 全局标志:是否已经发送过泄漏通知
    // 避免每个对象都单独发通知
    private var hasRetainedObjects = false
    
    @Synchronized
    private fun moveToRetained(key: String) {
        removeWeaklyReachableObjects()
        val retainedRef = watchedObjects[key]
        
        if (retainedRef != null) {
            retainedRef.retainedUptimeMillis = clock.uptimeMillis()
            retainedObjects.add(key)
            
            if (!hasRetainedObjects) {
                hasRetainedObjects = true
                // 只在首次发现泄漏时通知
                onObjectRetainedListeners.forEach {
                    it.onObjectRetained()
                }
            }
        }
    }
    
    // Dump完成后重置标志
    fun clearRetainedObjects() {
        hasRetainedObjects = false
        retainedObjects.clear()
    }
}

时序设计的关键考量:

时间轴(以Activity泄漏为例):

t=0ms    Activity.onDestroy()
         → expectWeaklyReachable(activity)
         → 创建KeyedWeakReference + 调度5秒后检查

t=500ms  (期间)GC可能自动发生
         → 如果回收了 → WeakReference自动入队

t=5000ms 第一次检查
         → removeWeaklyReachableObjects()
         → 检查引用队列
         → 如果不在 → 手动触发 GC

t=5100ms 第二次检查(GC后100ms)
         → removeWeaklyReachableObjects() 再次清理
         → 如果还在 → 确认泄漏 → retainedCount++

t=6000ms (假设) 累积5个泄漏对象
         → 触发 HeapDump

为什么选5秒?
  1. 给GC足够的自然执行时间(大部分对象5秒内被回收)
  2. 5秒是用户操作间隙的合理时间
  3. 过短 → GC没来得执行 → 误报
  4. 过长 → 泄漏对象占用内存时间久 → 增加OOM风险
  5. 可通过 config.watchDurationMillis 自定义

# 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

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中 → 对象未回收(可能泄漏)

JVM层面ReferenceQueue入队机制深入分析:

ReferenceQueue并非普通的链表队列,它的入队操作是由JVM的GC直接驱动的:

JVM Reference处理全景图:

┌─────────────────────────────────────────────────────────┐
│                    GC线程(虚拟机层面)                     │
│                                                         │
│  GC标记阶段:遍历所有可达对象                              │
│    ├── 从GC Root出发,标记所有可达对象(强引用)             │
│    └── 未被标记的对象 → 判定为垃圾                          │
│                                                         │
│  GC回收阶段:                                             │
│    ├── 检查垃圾对象是否被Reference子类引用                 │
│    │   ├── WeakReference → 对象finalize后回收              │
│    │   ├── SoftReference → 内存充足时不回收               │
│    │   └── PhantomReference → 仅跟踪回收时机               │
│    │                                                      │
│    └── 对于被WeakReference引用的对象:                     │
│        ├── 回收该对象 ← 释放内存                           │
│        └── 将WeakReference放入关联的ReferenceQueue          │
│                                                         │
└─────────────────────────────────────────────────────────┘
         │
         ▼  ReferenceQueue内部数据结构
┌─────────────────────────────────────────────────────────┐
│              ReferenceQueue (JVM实现)                     │
│                                                         │
│  内部结构:单链表                                          │
│  ┌──────┐   ┌──────┐   ┌──────┐                        │
│  │ head │──→│ ref1 │──→│ ref2 │──→ null                │
│  └──────┘   └──────┘   └──────┘                        │
│                                                         │
│  入队(enqueue):GC线程调用 Reference.enqueue()           │
│    └── 将Reference插入链表尾部                            │
│    └── synchronized保证线程安全                           │
│                                                         │
│  出队(poll/remove):应用线程调用                         │
│    ├── poll(): 非阻塞,返回null如果队列空                  │
│    └── remove(): 阻塞等待,直到有元素入队                  │
│                                                         │
└─────────────────────────────────────────────────────────┘

ART虚拟机中Reference的特殊处理:

Android ART虚拟机对Reference有专门的处理逻辑,位于 runtime/gc/reference_processor.cc:

ART ReferenceProcessor 处理流程:

1. GC发现仅有WeakReference引用的对象
   → 不立即回收(需要先处理ReferenceQueue)

2. GC调用 ReferenceProcessor::ProcessReferences()
   → 遍历所有待处理的Reference
   → 对于SoftReference:检查内存压力,决定是否保留引用
   → 对于WeakReference:清除引用(referent = null)
   → 调用 Reference.enqueue() 将引用加入队列

3. Java层通过queue.poll()感知回收事件

关键设计:
  → GC线程直接操作Java对象(ReferenceQueue链表)
  → 不需要Java层的线程协调
  → 效率高,无上下文切换开销

为何WeakReference的get()可能返回非null但不在队列中?

这是LeakCanary判断中的边缘情况:

// 场景:GC进行中,引用尚未入队
WeakReference<Object> ref = new WeakReference<>(obj, queue);
// ... GC发生,obj被标记为垃圾 ...
// GC线程正在清除引用,但尚未完成enqueue操作

// 此时:
ref.get()       // 返回 null(引用已被清除)
queue.poll()    // 返回 null(引用尚未入队!)

// 这是RTFM(Race Between GC and Application Thread)
// LeakCanary的处理方式:
//   等待5秒 + 多次GC + sleep(100ms)
//   让GC有足够时间完成整个引用处理流程
//   避免因时序问题产生误报

ReferenceHandler守护线程:

Java虚拟机内部有一个名为"Reference Handler"的守护线程,专门负责处理引用对象的pending队列:

ReferenceHandler工作流程(简化版):

1. JVM维护一个全局的pending链表
   └── GC线程将待处理的Reference放入此链表

2. ReferenceHandler守护线程持续运行
   └── 从pending链表取出Reference
   └── 对于WeakReference:清除referent字段
   └── 调用 enqueue() 将引用放入用户指定的ReferenceQueue

3. 应用线程通过queue.poll()获取

延迟分析:
  GC回收对象 → pending链表 → ReferenceHandler处理 → ReferenceQueue
  这个链路存在约1-50ms的不确定性延迟
  LeakCanary的100ms等待时间就是为了覆盖这个延迟

# 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,保证是最短路径

为什么选择BFS而不是DFS?

BFS能保证找到的是最短引用链,这对于开发者排查问题非常重要。最短引用链意味着最直接的泄漏原因,减少了排查的干扰信息。

Shark中PathFinder的完整实现分析:

// Shark引擎中PathFinder的核心实现(简化版)
class PathFinder(
    private val graph: HeapGraph,
    private val listener: PathFindingListener
) {
    // 结果容器
    private val pathsToLeakingObjects = mutableListOf<PathFindingResult>()
    
    fun findPaths(
        leakingObjectIds: Set<Long>,
        computeRetainedHeapSize: Boolean = false
    ): PathFindingResults {
        
        // 1. 获取所有GC Root
        val gcRoots = graph.gcRoots.toList()
        
        // 2. 初始化BFS队列(使用优先级队列支持不同引用类型)
        val queue = PriorityQueue<BfsNode>(compareBy { it.distance })
        
        // 3. 将所有GC Root加入队列作为起点
        for (gcRoot in gcRoots) {
            queue.offer(BfsNode(gcRoot.id, gcRoot, null, 0))
        }
        
        // 4. BFS主循环
        val visited = mutableSetOf<Long>()
        val parentMap = mutableMapOf<Long, BfsNode>()
        val foundLeakingIds = mutableSetOf<Long>()
        
        while (queue.isNotEmpty() && foundLeakingIds.size < leakingObjectIds.size) {
            val current = queue.poll()
            
            if (current.objectId in visited) continue
            visited.add(current.objectId)
            
            // 检查是否到达泄漏对象
            if (current.objectId in leakingObjectIds) {
                foundLeakingIds.add(current.objectId)
                // 重建路径
                val path = reconstructPath(current)
                pathsToLeakingObjects.add(
                    PathFindingResult(current.objectId, path)
                )
                continue
            }
            
            // 遍历当前对象的所有引用字段
            val heapObject = graph.findObjectById(current.objectId)
            for (reference in heapObject.readReferences()) {
                val childId = reference.valueObjectId
                if (childId !in visited && childId != ValueHolder.NULL_REFERENCE) {
                    parentMap[childId] = current
                    // 距离计算考虑引用类型权重
                    // 强引用权重最低(0),优先遍历
                    // 软引用权重高(2),延后遍历
                    val weight = when (reference.referenceType) {
                        ReferenceType.STRONG -> 0
                        ReferenceType.SOFT -> 2
                        ReferenceType.WEAK -> 4
                    }
                    queue.offer(
                        BfsNode(childId, reference, current, 
                                current.distance + weight)
                    )
                }
            }
        }
        
        return PathFindingResults(pathsToLeakingObjects, foundLeakingIds)
    }
    
    // 从BFS节点反向重建引用路径
    private fun reconstructPath(endNode: BfsNode): ShortestPath {
        val nodes = mutableListOf<LeakTraceObject>()
        var current: BfsNode? = endNode
        
        while (current != null) {
            nodes.add(
                LeakTraceObject(
                    type = current.type,
                    reference = current.reference,
                    leakingStatus = determineLeakingStatus(current)
                )
            )
            current = current.parent
        }
        
        return ShortestPath(nodes.reversed())  // 反转得到 GC Root → 泄漏对象
    }
}

引用类型优先级的BFS策略:

Shark的BFS不是普通的等权BFS,而是带权重的优先搜索:

引用类型权重分配:

ReferenceType.STRONG  → 权重 0(最高优先级,优先搜索)
  → 强引用路径才是真正导致泄漏的原因
  → BFS优先探索强引用路径

ReferenceType.SOFT    → 权重 2
  → 软引用路径次之

ReferenceType.WEAK    → 权重 4(最低优先级)
  → 弱引用路径不构成真正的泄漏(因为GC会回收WeakReference)

为什么带权BFS?
  → 找到的路径必须包含至少一条强引用链
  → 如果最短路径都是弱引用/软引用 → 对象不该泄漏
  → 带权BFS保证找到的是"最强最短"引用链

Shark如何高效读取对象引用关系:

// Shark按需读取对象引用(Lazy Loading)
fun HeapObject.readReferences(): Sequence<HeapReference> {
    return when (this) {
        is HeapInstance -> {
            // 实例对象:读取所有引用类型字段
            val record = hprofIndex.readInstanceRecord(objectId)
            record.fieldValues
                .filter { it.type == HprofValueType.OBJECT }
                .map { field ->
                    HeapReference(
                        name = field.fieldName,
                        valueObjectId = field.value.asObjectId,
                        referenceType = ReferenceType.STRONG
                    )
                }
        }
        is HeapObjectArray -> {
            // 对象数组:遍历所有元素
            val record = hprofIndex.readObjectArrayRecord(objectId)
            record.elementIds.mapIndexed { index, elementId ->
                HeapReference(
                    name = "[$index]",
                    valueObjectId = elementId,
                    referenceType = ReferenceType.STRONG
                )
            }
        }
        is HeapPrimitiveArray -> {
            // 基本类型数组:没有对象引用
            emptySequence()
        }
        is HeapClass -> {
            // Class对象:读取静态字段的引用
            val record = hprofIndex.readClassRecord(objectId)
            record.staticFields
                .filter { it.type == HprofValueType.OBJECT }
                .map { field ->
                    HeapReference(
                        name = field.fieldName,
                        valueObjectId = field.value.asObjectId,
                        referenceType = ReferenceType.STATIC_FIELD
                    )
                }
        }
    }
}

# 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到文件对应位置读取
  → 避免将整个堆加载到内存

HprofIndex 索引构建的完整原理:

HprofIndex是Shark引擎的核心索引结构,决定了分析效率:

// HprofIndex的索引结构(简化版)
class HprofIndex private constructor(
    private val hprof: Hprof,
    private val indexedGcRootTags: Set<HprofRecordTag>
) {
    // ===== 核心索引数据结构 =====
    
    // 1. String索引:字符串ID → 字符串内容
    //    用于将hprof中的字符串ID解析为可读的类名/字段名
    private val stringIndex: Map<Long, String>
    
    // 2. Class索引:类ID → HprofRecord(类定义Record)
    //    存储类的完整定义(父类、字段、静态字段值)
    private val classIndex: Map<Long, HprofRecord.HeapDumpRecord.ClassRecord>
    
    // 3. Instance索引:对象ID → (文件偏移量, 类ID)
    //    核心索引!记录每个实例对象在hprof文件中的位置
    //    不存储对象的具体字段数据 → 内存节省
    private val instanceIndex: Map<Long, LongOffsetToClassId>
    
    // 4. Object Array索引:数组ID → (文件偏移量, 元素类型)
    private val arrayIndex: Map<Long, LongOffsetToArrayType>
    
    // 5. Primitive Array索引:基本类型数组ID → 文件偏移量
    private val primitiveArrayIndex: Map<Long, Long>
    
    // 6. GC Root列表:提前存储的GC Root信息
    private val gcRoots: List<GcRoot>
    
    // 按需读取实例数据
    fun readInstanceRecord(objectId: Long): HprofRecord.InstanceRecord {
        val (offset, classId) = instanceIndex[objectId] 
            ?: throw IllegalStateException("Unknown object: $objectId")
        // 通过索引记录的偏移量,直接seek到文件位置读取
        return hprof.readInstanceRecordAt(offset, objectId, classId)
    }
    
    // 按需读取对象数组数据
    fun readObjectArrayRecord(objectId: Long): HprofRecord.ObjectArrayRecord {
        val (offset, arrayType) = arrayIndex[objectId]
            ?: throw IllegalStateException("Unknown array: $objectId")
        return hprof.readObjectArrayRecordAt(offset, objectId, arrayType)
    }
}

// 轻量级数据结构
data class LongOffsetToClassId(
    val offset: Long,       // 文件偏移量(8字节)
    val classId: Long       // 类ID(8字节)
)  // 每条记录仅16字节!vs 完整加载可能需要KB级

第一次遍历 - 索引构建的详细流程:

第一次遍历(索引构建)流程:

┌─────────────────────────────────────────────────────┐
│ 打开hprof文件                                       │
│   ↓                                                 │
│ 读取文件头(format, idSize, timestamp)              │
│   ↓                                                 │
│ while (还有Record可读):                              │
│   ├── 读取Record Header(tag + time + length)       │
│   │                                                   │
│   ├── STRING Record:                                │
│   │   → stringIndex[StringId] = "com.example.Activity"│
│   │                                                   │
│   ├── LOAD_CLASS Record:                            │
│   │   → 暂存classSerialNumber到classId的映射          │
│   │                                                   │
│   ├── CLASS_DUMP Record:                            │
│   │   → classIndex[ClassId] = ClassRecord(...)      │
│   │   → 记录父类、字段定义、静态字段值                │
│   │                                                   │
│   ├── INSTANCE_DUMP Record:                         │
│   │   → instanceIndex[ObjectId] = (offset, classId) │
│   │   → 关键:只记偏移量,不解析字段数据!            │
│   │                                                   │
│   ├── OBJECT_ARRAY_DUMP Record:                     │
│   │   → arrayIndex[ArrayId] = (offset, elementType) │
│   │                                                   │
│   └── 跳过多余记录(如HEAP_DUMP_INFO)               │
│                                                       │
│ 完成 → 关闭输入流                                    │
└─────────────────────────────────────────────────────┘

内存对比:
  hprof文件: 50MB
  haha全量加载:  ~300MB(对象+HashMap开销)
  Shark索引加载: ~15MB(仅索引结构)

第二次遍历(按需)的优化技巧:

// Shark按需读取的关键:基于索引的随机访问
fun readInstanceFieldValues(offset: Long, classId: Long): List<FieldValue> {
    // 1. 通过索引获取文件偏移量
    // 2. 使用Okio的BufferedSource + seek()定位到指定位置
    return hprofFile.source().use { source ->
        val bufferedSource = source.buffer()
        
        // 跳到实例数据的起始位置
        bufferedSource.skip(offset)
        
        // 从ClassIndex获取该类的字段定义
        val classRecord = classIndex[classId]!!
        val fields = classRecord.fields  // List<Field>
        
        // 按字段定义顺序读取值
        fields.map { field ->
            val type = field.type
            val value = when (type) {
                BasicType.OBJECT -> {
                    val refId = bufferedSource.readId()
                    // 如果是0(null引用),不需要进一步索引查找
                    if (refId == 0L) FieldValue.NullReference
                    else FieldValue.Reference(refId)
                }
                BasicType.BOOLEAN -> FieldValue.Boolean(bufferedSource.readByte() != 0.toByte())
                BasicType.INT -> FieldValue.Int(bufferedSource.readInt())
                BasicType.LONG -> FieldValue.Long(bufferedSource.readLong())
                // ... 其他基本类型
            }
            FieldValue(field.name, type, value)
        }
    }
}

Okio在Shark中的应用:

Shark选择Okio作为IO层,而非Java标准IO,有以下优势:

  1. 零拷贝Buffer:Buffer类内部使用链表管理数据分段,避免数据拷贝
  2. 高效的seek操作:基于偏移量的精确定位,避免重复读取
  3. 内存可控:分段读取大文件,不会占用连续大块内存
  4. 协程友好:可集成Kotlin协程实现非阻塞IO

# 4.4 提高Dump分析效率

Dump分析是十分耗时的行为。由于LeakCanary分析堆快照的过程存在一定的内存消耗,整个分析过程一般会持续几十秒,对于一些性能差的机型会造成明显的卡顿甚至ANR。

优化方案:

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

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

# 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")

签名相同 → 归为同一组 → 只需排查一次

# 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

# 4.7 GC触发与等待策略深度解析

LeakCanary为何需要手动触发GC?为什么需要等待100ms?为什么需要多次GC?这些设计背后有深刻的技术考量。

System.gc() vs Runtime.gc() 的选择:

// LeakCanary实际使用的方式
Runtime.getRuntime().gc()  // ← 选这个
Runtime.getRuntime().runFinalization()

// 为什么不直接用 System.gc()?
// System.gc() 内部调用了 Runtime.getRuntime().gc()
// 两者功能完全等价,但Runtime.gc()更明确

// 关键注解:当前虚拟机的GC实现
// HotSpot/ART 中 gc() 默认触发 Full GC
//    → 如果虚拟机配置 -XX:+DisableExplicitGC
//    → System.gc() 将被忽略!这是线上环境的常见设置
//    → 这也是LeakCanary不适合线上的原因之一

GC触发策略的完整流程分析:

LeakCanary的GC触发与等待策略(三次GC重试机制):

第一次检查(5秒后):
  ├── removeWeaklyReachableObjects()  → 清理已入队的引用
  ├── watchedObjects中还有目标对象?
  │   ├── 否 → 正常结束
  │   └── 是 → 进入GC重试流程
  │
  ├── GC重试 Round 1:
  │   ├── Runtime.getRuntime().gc()      ← 请求Full GC
  │   ├── Thread.sleep(100)               ← 等待GC完成
  │   ├── Runtime.getRuntime().runFinalization()  ← 加速finalize
  │   └── removeWeaklyReachableObjects()  → 检查结果
  │        ├── 已回收 → 正常结束
  │        └── 未回收 → 进入Round 2
  │
  ├── GC重试 Round 2:
  │   └── 重复Round 1的步骤
  │        ├── 已回收 → 正常结束
  │        └── 未回收 → 进入Round 3
  │
  └── GC重试 Round 3:
      └── 重复Round 1的步骤
           ├── 已回收 → 正常结束
           └── 未回收 → 确认泄漏!→ retainedCount++

为什么需要100ms等待?

GC执行的时间窗口分析:

t=0ms     Runtime.gc() 调用
          → 设置 GC 标志位
          → 不保证立即执行
          
t=0~10ms  虚拟机调度GC线程
          → GC线程被唤醒
          → 准备执行GC
          
t=10~50ms GC标记阶段
          → 标记所有可达对象
          → 包括finalize队列的处理
          
t=50~80ms GC清理阶段
          → 回收不可达对象内存
          → ReferenceHandler处理引用
          → 将WeakReference放入ReferenceQueue
          
t=80~100ms GC完成
          → 内存释放完成
          → 引用队列更新完成

100ms的等待时间:
  → 覆盖了 ART GC 最坏情况下的完整执行时间
  → 考虑了 ReferenceHandler 的处理延迟
  → 考虑了 finalize 方法的执行时间
  → 100ms对用户体验几乎无感知

为什么可能需要3次GC?

Java GC的"不保证"特性:

为什么一次GC不够?

场景1: Young GC vs Full GC
  Runtime.gc() 请求Full GC,但虚拟机可能:
  ├── 只执行 Young GC(轻量级)
  │   └── Young Gen中的WeakReference被回收 → 入队 ✓
  └── Old Gen未被扫描
      └── Old Gen中的WeakReference未被处理 → 仍在监控中 ✗

场景2: Finalize延迟
  有finalize()方法的对象需要两次GC:
  GC#1 → 标记对象为finalizable → 放入finalization队列
        → 等待finalizer线程执行finalize()
  GC#2 → finalize()已执行 → 真正回收对象 → 引用入队
  
  runFinalization() 可以加速这个过程,但不保证同步完成

场景3: 并发GC的不确定性
  ART的并发GC(CMS, CC):
  → GC与应用线程并发执行
  → 标记阶段不暂停应用
  → 只有最终清理阶段短暂暂停
  → 引用入队可能延迟到下一轮GC

三次GC重试覆盖了以上所有情况的最坏组合

LeakCanary中运行时GC的关闭机制:

// 可通过配置禁用运行时GC(某些测试场景)
objectWatcher.config = objectWatcher.config.copy(
    // 是否在每个Activity.onDestroy()时触发GC
    runGCWhenExceedingThreshold = true  // 默认true
)

// 或者在LeakCanary Config中控制
LeakCanary.config = LeakCanary.config.copy(
    dumpHeap = true,
    // 是否在dump前手动触发GC
    runGCBeforeDump = true
)

# 4.8 堆快照解析的两次遍历策略深度解析

Shark引擎采用"两次遍历 + 按需加载"策略,这是它比haha快6倍的核心秘密。本节深入剖析这个设计的原理和实现。

问题背景:hprof文件的特性

hprof文件是一个线性的二进制流,Record按时间顺序排列,没有随机访问结构:

hprof文件的线性特性造成的挑战:

┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
│ STRING       │ LOAD_CLASS   │ INSTANCE     │ ARRAY        │ CLASS_DUMP   │
│ "Activity"   │ 0x1234       │ 0xAABB data  │ 0xCCDD data  │ 0x1234       │
└──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
        ↑ 第1个Record      ↑ 第100个Record  ↑ 第50000个Record  ↑ 第80000个Record

挑战:
  1. 对象的类定义(CLASS_DUMP)可能在实例数据(INSTANCE)之后
     → 必须先把所有CLASS_DUMP找出来才能解析INSTANCE
  
  2. BFS搜索时,需要访问:当前对象 → 所有引用的子对象
     如果每次访问都遍历整个文件 → 时间复杂度O(n²) → 不可接受
  
  3. 堆可能很大(300MB+),全部加载到内存 → OOM

Shark两次遍历的设计原理:

第一次遍历(Build Index):O(n) 顺序读

目的:构建所有对象的"文件位置目录"

┌──────────────────────────────────────────────────────────┐
│                  HprofIndex 结构                          │
│                                                          │
│  instanceIndex: Map<ObjectId, ObjectPosition>             │
│  ┌─────────┬─────────────────────┬──────────────┐        │
│  │ 0xAABB  │ offset=102400, cid= │ 0x1234       │        │
│  │ 0xBBCC  │ offset=204800, cid= │ 0x5678       │        │
│  │ 0xCCDD  │ offset=307200, cid= │ 0x1234       │        │
│  │ 0xDDEE  │ offset=409600, cid= │ 0x9ABC       │        │
│  │ ...     │ ...               │ ...           │        │
│  └─────────┴─────────────────────┴──────────────┘        │
│                                                          │
│  每个条目:ObjectId(8B) + Offset(8B) + ClassId(8B)        │
│  = 24字节/对象                                            │
│  500,000个对象 × 24字节 = 仅12MB                          │
│  vs 完整加载到内存:可能需要200MB+                          │
└──────────────────────────────────────────────────────────┘

第二次遍历(On-demand):O(k) 按需读

BFS搜索时,需要查看对象0xAABB的引用字段:

  1. instanceIndex[0xAABB] → (offset=102400, classId=0x1234)
  2. classIndex[0x1234] → fields: [name:String, age:int, manager:Employee]
  3. 文件.seek(102400) → 按字段顺序读取实例值
  4. 返回引用列表:[childId1, childId2, ...]

  关键:只读取当前被访问的对象,不加载整个堆

索引存储的高效实现:

// Shark索引设计的核心优化

// 1. 针对密集ID的优化存储
// 对象ID在hprof文件中通常是密集排列的
// (如 0x1000, 0x1008, 0x1010, ...)
// 使用数组代替HashMap可以大幅节省内存

class DenseObjectIndex(
    private val idSize: Int,
    private val baseId: Long,
    private val offsets: LongArray,  // 按ID偏移量直接数组访问
    private val classIds: LongArray
) {
    // O(1) 查找,无HashMap开销
    fun getOffset(objectId: Long): Long {
        val index = ((objectId - baseId) / idSize).toInt()
        return offsets[index]
    }
    
    // 内存对比:
    // HashMap<Long, Long> : 每个Entry ~56字节(Node对象+Long key+Long value)
    // LongArray : 每个元素 8字节
    // 内存节省约 7倍!
}

// 2. 针对稀疏ID的优化存储
// 如果对象ID分布稀疏,使用排序数组+二分查找
class SparseObjectIndex(
    private val ids: LongArray,      // 排序后的ID数组
    private val offsets: LongArray,  // 对应的偏移量
    private val classIds: LongArray
) {
    // O(log n) 查找,内存紧凑
    fun getOffset(objectId: Long): Long {
        val index = ids.binarySearch(objectId)
        return if (index >= 0) offsets[index]
               else throw ObjectNotFoundException(objectId)
    }
}

实际性能对比数据:

Benchmark: 分析 50MB hprof 文件,包含约600,000个对象

| 指标              | haha (1.x)    | Shark (2.x)    | 提升    |
|------------------|--------------|---------------|---------|
| 索引阶段耗时       | 直接加载       | 2.3秒          | N/A     |
| 内存峰值          | ~280MB        | ~45MB          | 6.2倍   |
| BFS搜索耗时       | 8.5秒          | 1.2秒          | 7.1倍   |
| 总分析耗时         | 12秒          | 3.5秒           | 3.4倍   |
| 是否可在低端机运行  | × (OOM)      | ✓               | -       |

注:haha将整个堆加载到内存后才开始分析,Shark边加载边分析

Shark的两阶段分析Pipeline设计:

分析Pipeline的流水线设计:

阶段1: IndexingPhase (索引阶段)
  ┌─────────────────────────────────┐
  │ HprofFileReader → 顺序扫描hprof  │
  │ ├── 按顺序读取每个Record         │
  │ ├── 提取关键索引信息             │
  │ └── 构建HprofIndex              │
  └────────────┬────────────────────┘
               │ HprofIndex
               ▼
阶段2: AnalysisPhase (分析阶段)
  ┌─────────────────────────────────┐
  │ PathFinder → BFS搜索引用链       │
  │ ├── 从索引获取对象位置            │
  │ ├── seek到文件位置按需读取        │
  │ └── 构建最短引用链               │
  └────────────┬────────────────────┘
               │ List<ShortestPath>
               ▼
阶段3: ReportingPhase (报告阶段)
  ┌─────────────────────────────────┐
  │ LeakTraceRenderer → 生成报告     │
  │ ├── 格式化为可读的引用链          │
  │ ├── 标记怀疑对象                 │
  │ └── 计算泄漏签名用于分组          │
  └─────────────────────────────────┘

Shark支持的分析类型扩展:

Shark不仅是泄漏分析工具,还支持多种堆分析场景:

// Shark支持的分析类型
sealed class HeapAnalysis {
    // 1. 泄漏分析(LeakCanary主场景)
    data class LeakAnalysis(
        val leakTraces: List<LeakTrace>,
        val leakCount: Int,
        val metadata: Map<String, String>
    ) : HeapAnalysis()
    
    // 2. 堆增长分析(找出堆中最大的对象)
    data class HeapGrowthAnalysis(
        val topGrowingObjects: List<ObjectGrowth>,
        val growthRate: Double,
        val metadata: Map<String, String>
    ) : HeapAnalysis()
    
    // 3. 元数据分析(统计类实例数量、堆中对象分布)
    data class MetadataAnalysis(
        val classCount: Map<String, Int>,
        val totalInstanceCount: Long,
        val totalHeapSize: Long,
        val metadata: Map<String, String>
    ) : HeapAnalysis()
}

# 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等复杂组件
   → 降低使用者的学习成本

# 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修饰符防止外部继承和使用
// 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更灵活,每个子类可以有不同字段
// → 比普通继承更安全,限制了子类范围

# 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可以用于任何接口

# 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不受系统时间修改影响
//    避免用户手动改时间导致判断逻辑出错

# 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. 这是整洁架构中"依赖倒置原则"的体现

# 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(懒序列)遍历字段引用
   → 大幅减少内存占用
   → 适合在移动设备上运行

# 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       │
└─────────────────────────────────────────────┘

# 6.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(引用链节点)

# 6.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页面)

# 6.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

# 6.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框架泄漏的自动修复

# 07.其他设计说明

# 7.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方案避免锁堆

# 7.2 稳定性设计

LeakCanary在稳定性方面做了多方面考量:

稳定性设计:

1. 异常兜底
   └── 所有Hook操作都有try-catch保护
   └── 分析失败不会导致应用崩溃
   └── Shark分析异常会输出错误信息而非Crash

2. 内存保护
   └── Shark使用按需加载,避免OOM
   └── hprof文件分析完毕后及时删除
   └── 限制最大保留的hprof文件数量

3. 线程安全
   └── ObjectWatcher的watchedObjects使用synchronized保护
   └── ReferenceQueue的poll操作是线程安全的
   └── 泄漏计数使用原子操作

4. 版本兼容
   └── 不同Android版本的Hook策略不同
   └── 对反射调用做版本适配
   └── 优雅降级:Hook失败时不影响应用正常运行

# 7.3 灰度设计

在实际项目中使用LeakCanary时的灰度策略:

灰度策略建议:

1. Debug包默认开启
   └── 开发和测试阶段全量开启
   └── 帮助尽早发现泄漏

2. Release包关闭
   └── LeakCanary默认只在debug包中生效
   └── debugImplementation 'com.squareup.leakcanary:...'
   └── Release包不包含LeakCanary代码

3. 线上灰度(使用KOOM等替代方案时)
   └── 先灰度1%用户
   └── 观察性能影响
   └── 逐步扩大灰度比例
   └── 设置采样率控制

# 7.4 降级设计

降级策略:

1. 设备性能降级
   └── 低端设备(RAM < 3GB)可以关闭检测
   └── 减少监控的组件类型(只监控Activity)

2. 阈值降级
   └── 提高泄漏对象触发Dump的阈值(从5提高到10)
   └── 增加两次Dump的最小间隔(从60s提高到120s)

3. 分析降级
   └── 在主进程内存紧张时推迟分析
   └── 使用独立进程分析,不影响主进程

# 7.5 异常设计优化

异常处理策略:

1. Hook失败处理
   └── 反射Hook失败时优雅降级
   └── 不影响应用正常功能
   └── 输出日志供开发者排查

2. Dump失败处理
   └── Debug.dumpHprofData()可能抛出IOException
   └── 磁盘空间不足时的处理
   └── 失败后重新调度下一次Dump

3. 分析失败处理
   └── hprof文件损坏的处理
   └── 分析超时的处理
   └── 内存不足时的处理(中断分析)

关于finalize方法的补充说明:

LeakCanary在触发GC后还调用了System.runFinalization(),这是强制调用已失去引用对象的finalize方法。在可达性算法中,不可达对象也不是立即死亡的,需要经过两次标记过程。第一次标记后会筛选是否需要执行finalize()方法,如果对象没有覆盖finalize()或者finalize()已经被调用过,则视为"没有必要执行",直接回收。调用runFinalization()可以加速这个过程,让判断更加准确。

# 08.线上内存泄漏检测方案

# 8.1 LeakCanary为何不能用于线上

核心原因总结:

LeakCanary不适合线上的原因:

性能影响:
├── GC触发:主动调用GC会导致应用短暂卡顿
├── 堆锁定:dumpHprofData()会Stop-The-World,冻结数秒
├── 分析耗时:Shark分析过程消耗CPU和内存
└── IO开销:hprof文件10-30MB,磁盘读写量大

用户体验:
├── 应用冻结导致用户感知卡顿
├── 前台通知干扰用户
└── 分析过程影响应用流畅性

资源消耗:
├── hprof文件占用大量磁盘空间
├── 分析过程可能导致内存峰值
└── 频繁GC影响内存分配效率

# 8.2 KOOM线上检测方案

快手KOOM(Kwai OOM)的核心创新是利用Linux的fork+COW机制实现无冻结的HeapDump:

KOOM的fork+COW核心原理:

KOOM工作流程:

1. 监控阶段
   └── 定期采样Java堆内存使用量
   └── 当内存使用量连续N次超过阈值时触发检测
   └── 比LeakCanary的对象级监控更轻量

2. Dump阶段(核心优化)
   └── 调用fork()创建子进程
   └── 子进程继承父进程的完整内存快照(COW)
   └── 父进程立即恢复执行(不受影响)
   └── 子进程中执行Debug.dumpHprofData()
   └── 子进程完成后自动退出

3. 分析阶段
   └── 在子进程或后台Service中分析hprof
   └── 使用裁剪后的hprof(只保留必要信息)
   └── 生成轻量级的分析报告

4. 上报阶段
   └── 只上报分析结果(JSON格式)
   └── 不上报hprof文件
   └── 通过网络上报到后端

fork+COW机制的深入分析:

Linux fork() + COW (Copy-On-Write) 原理:

1. fork() 系统调用:
   ┌──────────────────────────────────────────────┐
   │  用户空间                                      │
   │  pid_t pid = fork();                         │
   │  if (pid == 0) {                             │
   │      // 子进程                               │
   │  } else {                                    │
   │      // 父进程,pid > 0                       │
   │  }                                           │
   └───────────────┬──────────────────────────────┘
                   │ 系统调用
                   ▼
   ┌──────────────────────────────────────────────┐
   │  内核空间                                      │
   │                                              │
   │  do_fork() → copy_process()                  │
   │    ├── 复制 task_struct(进程描述符)          │
   │    ├── 创建新的内核栈                          │
   │    ├── 复制页表(Page Table)← 关键!          │
   │    │   └── 父子进程指向相同的物理页            │
   │    │   └── 页表项标记为Read-Only(触发COW)    │
   │    └── 设置进程关系                            │
   │                                              │
   │  COW触发时机:                                 │
   │    父/子进程尝试写入某个物理页                  │
   │    → MMU检测到写保护                           │
   │    → 触发页面错误(Page Fault)                │
   │    → 内核分配新的物理页                        │
   │    → 复制原始页内容到新页                       │
   │    → 更新页表,指向新页                        │
   │    → 恢复进程执行                              │
   │                                              │
   └──────────────────────────────────────────────┘

   COW的核心优势:
   ┌──────────────────────────────────────────────┐
   │ fork前:父进程内存占用 300MB                    │
   │ fork后:父进程 + 子进程 实际物理内存 ≈ 300MB   │
   │         (只有页表额外开销,~几MB)               │
   │                                              │
   │ 子进程只做 hprof dump(只读操作):             │
   │  → 遍历堆对象 → 纯读取 → 不触发COW             │
   │  → 写入hprof文件 → 写子进程自己的内存 → 不触发COW│
   │  → 物理内存几乎不增加!                         │
   │                                              │
   │ 父进程继续正常执行:                           │
   │  → 分配新对象 → 写操作 → 触发COW               │
   │  → 每次COW只复制1页(4KB)                       │
   │  → 非常分散,性能影响微小                      │
   └──────────────────────────────────────────────┘

KOOM的完整Dump流程代码分析:

// KOOM fork子进程进行HeapDump的核心代码(简化版)
public class ForkJvmHeapDumper {
    
    public boolean dump(String path) {
        // 1. 暂停所有Java线程(短暂)
        //    配合fork一起使用,保证内存快照一致性
        suspendAllThreads();
        
        // 2. fork子进程
        int pid = fork();
        
        if (pid == 0) {
            // ===== 子进程 =====
            // 恢复子进程中的Java线程
            resumeAllThreadsInChild();
            
            try {
                // 在子进程中执行HeapDump
                // 此时子进程拥有父进程的完整内存快照(COW)
                Debug.dumpHprofData(path);
                
                // 分析hprof文件(可选,也可留给Service)
                // analyzeHprof(path);
                
            } catch (Exception e) {
                KLog.e(TAG, "dump failed in child process", e);
            } finally {
                // 通过进程间通信通知父进程dump完成
                notifyParentProcess();
                // 子进程退出
                System.exit(0);
            }
        } else if (pid > 0) {
            // ===== 父进程 =====
            // 立即恢复所有线程
            resumeAllThreads();
            
            // 父进程继续正常运行
            // 通过wait或异步回调等待子进程完成
            // int status = waitpid(pid, ...);
            
            return true;
        } else {
            // fork失败
            resumeAllThreads();
            return false;
        }
    }
}

为什么要先suspend再fork?

suspend + fork 的组合时序:

原因:保证内存快照的原子一致性

不加suspend:
  ┌──────────────┐      ┌──────────────┐
  │  父进程       │      │  子进程       │
  │              │      │              │
  │ [mutating]   │      │              │
  │ obj.field = x│──────│─ fork ───────│  子进程看到的可能是
  │   (修改中)    │      │  看到不一致状态 │  半完成的修改!
  └──────────────┘      └──────────────┘

加suspend:
  ┌──────────────┐      ┌──────────────┐
  │  父进程       │      │  子进程       │
  │              │      │              │
  │ ---suspend---│      │              │  所有线程暂停
  │     ↓        │      │              │  内存状态确定
  │    fork() ───│──────│→ 子进程创建  │  快照一致
  │  ---resume---│      │ dump开始      │  父进程立即恢复
  │ [mutating]   │      │              │
  └──────────────┘      └──────────────┘

  suspend时间通常 < 10ms(只复制页表,不复制内存)
  vs LeakCanary的dumpHprofData 冻结 2-5秒

hprof文件裁剪优化:

KOOM不是直接使用完整的hprof文件,而是进行裁剪:

hprof裁剪策略(减少文件体积):

完整hprof: 30MB  →  裁剪后hprof: 5-8MB

裁剪内容:
├── 只保留实例对象(INSTANCE_DUMP)
├── 只保留对象数组(OBJECT_ARRAY_DUMP)
├── 只保留类定义(CLASS_DUMP)
├── 只保留必需的字符串(STRING)
├── 去除基本类型数组(除非是大数组)
│   └── byte[] 通常占堆的50%+(图片、缓冲区),不需要泄漏分析
├── 去除线程栈信息(非必需)
└── 去除时间戳等元数据

裁剪方法:
1. 在fork子进程中执行dump
2. 使用HprofWriter重写hprof文件
3. 只写入分析需要的Record
4. 大幅减少IO和传输成本

KOOM的内存触发阈值策略:

KOOM的触发策略比LeakCanary更细致:

1. 内存监控数据源:
   ├── Runtime.getRuntime().totalMemory() - freeMemory()  (Java堆使用量)
   ├── Debug.getNativeHeapAllocatedSize()  (Native堆使用量)
   └── /proc/self/status → VmRSS (物理内存占用)

2. 阈值设定:
   ├── 绝对阈值:Java堆 > maxHeap × 85%
   │   └── 例如 maxHeap = 512MB → 触发阈值 = 435MB
   ├── 增长速率:5分钟内增长 > 100MB
   │   └── 排除短暂的内存峰值
   └── 连续N次超阈值才触发
       └── 避免因短暂GC导致误判

3. 采样间隔:
   ├── 正常状态:每30秒采样一次
   ├── 内存增长中:每10秒采样一次
   └── 内存高位:每5秒采样一次

# 8.3 线上内存泄漏监控体系

完整的线上内存监控体系:

采集层:
├── Java堆内存采样
├── Native内存采样
├── 大对象监控(>1MB的Bitmap等)
├── Activity/Fragment泄漏检测
└── 内存触顶告警

分析层:
├── KOOM离线分析
├── hprof裁剪与压缩
├── 引用链提取
└── 泄漏自动归类

上报层:
├── 采样率控制(按设备、版本、用户分层)
├── 流量控制(非WiFi环境不上报大文件)
├── 去重机制(相同泄漏只上报一次)
└── 数据脱敏

展示层:
├── 泄漏大盘(泄漏率趋势、Top N泄漏)
├── 版本对比(新版本是否引入新泄漏)
├── 详情页面(引用链、影响范围)
└── 告警通知(新泄漏自动告警)

# 8.4 各方案对比分析

内存泄漏检测方案对比:

| 特性          | LeakCanary    | KOOM          | Matrix        |
|---------------|--------------|--------------|---------------|
| 适用环境       | 开发/测试     | 线上          | 线上          |
| 检测方式       | 对象级监控     | 内存阈值触发  | 对象级+阈值   |
| Dump方式       | 主进程直接Dump | fork子进程    | fork子进程    |
| 应用冻结       | 数秒冻结      | 无冻结        | 无冻结        |
| 分析引擎       | Shark         | 自研          | 自研          |
| 性能影响       | 较大          | 小            | 小            |
| 接入成本       | 零配置        | 需要配置      | 需要配置      |
| 开源           | 是            | 是            | 是            |

# 09.常见面试题深度解析

# 9.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:WeakReference和ReferenceQueue的关系?

WeakReference指向的对象被GC回收时:
1. GC线程发现对象只有弱引用(无强引用/软引用)
2. 回收该对象
3. 将WeakReference放入关联的ReferenceQueue
4. 应用线程通过queue.poll()获取已入队的引用

LeakCanary利用此机制:
  创建WeakReference(obj, queue) → 等待GC
  → poll queue → 弱引用在队列中 → 对象已回收(正常)
  → poll queue → 弱引用不在队列中 → 对象未回收(泄漏)

问题3:LeakCanary如何做到零配置?

通过ContentProvider自动初始化:
1. 在AAR的AndroidManifest中注册AppWatcherInstaller(ContentProvider)
2. Android系统在创建Application时自动创建此ContentProvider
3. ContentProvider.onCreate()在Application.onCreate()之前调用
4. 在onCreate()中完成LeakCanary的所有初始化工作
5. 开发者只需添加依赖,无需任何初始化代码

# 9.2 进阶面试问题

问题4:LeakCanary为什么不能用于线上?KOOM是如何解决的?

LeakCanary不能用于线上的原因:
1. dumpHprofData()会锁堆,应用冻结数秒
2. 频繁GC影响性能
3. hprof文件大,占用磁盘和IO

KOOM的解决方案:
1. 使用fork()创建子进程
2. 子进程通过COW获得父进程内存快照
3. 父进程立即恢复,不受影响
4. 子进程中执行Dump和分析
5. 只上报分析结果,不上报hprof文件

问题5:LeakCanary如何监控ViewModel的销毁?

由于Android Framework未提供ViewModel.onCleared()的全局监听:

1. 在Activity/Fragment的onCreate中
   通过ViewModelProvider创建一个自定义ViewModelClearedWatcher

2. 当Activity/Fragment销毁时
   ViewModelClearedWatcher.onCleared()被调用

3. 在onCleared()中
   通过反射获取ViewModelStore内部的map
   遍历所有ViewModel对象
   将它们交给ObjectWatcher监控

问题6:Shark相比haha有什么优势?

Shark优势:
1. 按需加载:不一次性加载整个hprof到内存,使用索引+seek方式
2. 两次遍历:第一次建索引,第二次按需读取,减少IO操作
3. 使用Okio:高效的IO库,减少内存拷贝
4. Kotlin编写:更安全的空安全处理
5. 分析速度:比haha快约6倍
6. 内存消耗:远低于haha

# 9.3 学习建议

学习LeakCanary建议按以下顺序:

  1. 理解基础概念:Java引用类型、WeakReference、ReferenceQueue
  2. 掌握核心原理:WeakReference + ReferenceQueue的泄漏检测机制
  3. 学习初始化:ContentProvider自动初始化的设计思想
  4. 深入组件监听:各种Watcher如何Hook不同组件的生命周期
  5. 了解分析引擎:Shark如何解析hprof文件并查找引用链
  6. 扩展学习:KOOM等线上方案的fork+COW优化思路
上次更新: 2026/07/12, 19:39:51
README
Glide图片加载设计

← README Glide图片加载设计→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式