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.5 引用链如何生成
- 4.6 分析引擎的原理
- 4.7 提高Dump分析效率
- 4.8 相同问题分组
- 4.9 如何标记怀疑对象
- 05.方案基础设计
- 5.1 整体架构图
- 5.2 UML设计图
- 5.3 关键流程图
- 5.4 接口设计图
- 5.5 模块间依赖关系
- 06.其他设计说明
- 6.1 性能设计优化
- 6.2 稳定性设计
- 6.3 灰度设计
- 6.4 降级设计
- 6.5 异常设计优化
01.整体概述介绍
1.1 项目背景介绍
1.2 基础概念
- 什么是内存泄漏?
- 当我们App无法释放不需要的对象引用时,即为内存泄漏。也可以理解为:生命周期长的持有了生命周期短的对象所导致。
- 内存泄露(Memory Leaks)指不再使用的对象或数据没有被回收,随着内存泄漏的堆积,应用性能会逐渐变差,甚至发生 OOM 奔溃。
- 应用中的内存泄漏可以分为 2 类:
- Java 内存泄露: 不再使用的对象被生命周期更长的 GC Root 引用,无法被判定为垃圾对象而导致内存泄漏(LeakCanary 只能监控 Java 内存泄漏);
- Native 内存泄露: Native 内存没有垃圾回收机制,未手动回收导致内存泄漏。
1.3 设计目标
- LeakCanary 支持以下五种 Android 场景中的内存泄漏监测:
- 1、已销毁的 Activity 对象(进入 DESTROYED 状态);
- 2、已销毁的 Fragment 对象和 Fragment View 对象(进入 DESTROYED 状态);
- 3、已清除的的 ViewModel 对象(进入 CLEARED 状态);
- 4、已销毁的的 Service 对象(进入 DESTROYED 状态);
- 5、已从 WindowManager 中移除的 RootView 对象;
1.4 产生收益
- LeakCanary 的特点或优势
- 在于提前预判出 Android 应用中最常见且影响较大的内存泄漏场景,并对此做针对性的监测手段。更加高效。
02.开发设计思路
2.1 整体设计思路
- LeakCanary 的核心工作流程,将其概括为以下 5 个阶段:
image
- 1、注册无用对象监听:
- 在 Android Framework 中注册监听器,感知五种 Android 内存泄漏场景中产生无用对象的时机(例如在 Activity#onDestroy() 后,产生一个无用 Activity 对象);
- 2、监控内存泄漏:
- 为无用对象关联弱引用对象,如果一段时间后引用对象没有按预期进入引用队列,则认为对象发生内存泄漏。
- 由于分析堆快照是耗时工作,所以 LeakCanary 不会每次发现内存泄漏对象都进行分析工作,而是内存泄漏对象计数到达阈值才会触发分析工作。在计数未到达阈值的过程中,LeakCanary 会发送一条系统通知,你也可以点击该通知提前触发分析工作;
- 3、Java Heap Dump:
- 当泄漏对象计数达到阈值时,会触发 Java Heap Dump 并生成 .hprof 文件存储到文件系统中。Heap Dump 的过程中会锁堆,会使应用冻结一段时间;
- 4、分析堆快照:
- LeakCanary 会根据应用的依赖项,选择多进程、或 Thread 异步任务其中一种策略来执行分析。
- 分析过程 LeakCanary 使用 Shark 分析 .hprof 文件,替换了 LeakCanary 1.0 使用的 haha ;
- 5、输出分析报告:
- 当分析工作完成后,LeakCanary 会在 Logcat 打印分析结果,也会发送一条系统通知消息。点击通知消息可以跳转到可视化分析报告页面,也可以点击 LeakCanary 生成的桌面快捷方式进入。
2.2 设计初始化思路
- LeakCanary 的初始化工程可以概括为 2 项内容:
- 1、初始化 LeakCanary 内部分析引擎;
- 2、在 Android Framework 上注册多种 Android 泄漏场景的监控。
2.3 如何设计组件监听
- Activity 监控:
- 通过 Application#registerActivityLifecycleCallbacks(…) 接口监听 Activity#onDestroy 事件,将当前 Activity 对象交给 ObjectWatcher 监控;
- Fragment 与 Fragment View 监控:
- 首先是通过 Application#registerActivityLifecycleCallbacks(…) 接口监听 Activity#onCreate 事件,再通过 FragmentManager#registerFragmentLifecycleCallbacks(…) 接口监听 Fragment 的生命周期;
- ViewModel 监控:
- 由于 Android Framework 未提供设置 ViewModel#onClear() 全局监听的方法,所以 LeakCanary 是通过 Hook 的方式实现。
- 即:在 Activity#onCreate 和 Fragment#onCreate 事件中实例化一个自定义ViewModel,在进入 ViewModel#onClear() 方法时,通过反射获取当前作用域中所有的 ViewModel 对象交给 ObjectWatcher 监控。
- Service 监控:
- 由于 Android Framework 未提供设置 Service#onDestroy() 全局监听的方法,所以 LeakCanary 是通过 Hook 的方式实现的。
- RootView 监控:
- 由于 Android Framework 未提供设置全局监听 RootView 从 WindowManager 中移除的方法,所以 LeakCanary 是通过 Hook 的方式实现的,这一块是通过 squareup 另一个开源库 curtains 实现的。
2.4 设计无用对象监听
- 如何标记一个对象,判断是否泄漏。
- 为弱引用指定一个引用队列,当弱引用指向的对象被回收时,此弱引用就会被添加到这个队列中,我们可以通过判断这个队列中有没有这个弱引用,来判断该弱引用指向的对象是否被回收了。
- 通过弱引用判断内存是否泄漏案例
// 创建一个引用队列 ReferenceQueue<Object> queue = new ReferenceQueue<>(); private void test() { // 创建一个对象 Object obj = new Object(); // 创建一个弱引用,并指向这个对象,并且将引用队列传递给弱引用 WeakReference<Object> reference = new WeakReference(obj, queue); // 打印出这个弱引用,为了跟gc之后queue里面的对比证明是同一个 System.out.println("这个弱引用是:" + reference); // gc一次看看(毛用都没) System.gc(); // 打印队列(应该是空) printlnQueue("before"); // 先设置obj为null,obj可以被回收了 obj = null; // 再进行gc,此时obj应该被回收了,那么queue里面应该有这个弱引用了 System.gc(); // 再打印队列 printlnQueue("after"); } private void printlnQueue(String tag) { System.out.print(tag); Object obj; // 循环打印引用队列 while ((obj = queue.poll()) != null) { System.out.println(": " + obj); } System.out.println(); }
- 打印结果如下所示:
这个弱引用是:java.lang.ref.WeakReference@6e0be858 before after: java.lang.ref.WeakReference@6e0be858
- 通过上述代码,我们看到
- 当obj不为null时,进行gc,发现queue里面什么都没有;然后将obj置为null之后,再次进行gc,发现queue里面有这个弱引用了,这就说明obj已经被回收了
- 利用这个特性,我们就可以检测Activity 的内存泄漏
- Activity在onDestroy()之后被销毁,那么我们如果利用弱引用来指向Activity,并为它指定一个引用队列,然后在onDestroy()之后,去查看引用队列里是否有该Activity对应的弱引用,就能确定该Activity是否被回收了。
2.5 设计内存泄漏监听
- 1、在 Android Framework 中注册无用对象监听:
- 通过全局监听器或者 Hook 的方式,在 Android Framework 上监听 Activity 和 Service 等对象进入无用状态的时机(例如在 Activity#onDestroy() 后,产生一个无用 Activity 对象);
- 2、利用引用对象可感知对象垃圾回收的机制判定内存泄漏:
- 为无用对象包装弱引用,并在一段时间后(默认为五秒)观察弱引用是否如期进入关联的引用队列,是则说明未发生泄漏,否则说明发生泄漏(无用对象被强引用持有,导致无法回收,即泄漏)。
- LeakCanary 发现泄漏对象后就会触发分析吗?
- Watcher 判定被监控对象发生泄漏后,会做一些判断逻辑。LeakCanary 不会每次发现内存泄漏对象都进行分析工作,而会进行两个拦截:
- 拦截 1:泄漏对象计数未达到阈值,或者进入后台时间未达到阈值;拦截 2:计算距离上一次 HeapDump 未超过 60s。
2.6 设计Dump内存
- LeakCanary 已经成功生成 .hprof 堆快照文件,并且发送了一个 LeakCanary 内部事件 HeapDump。那么这个事件在哪里被消费的呢?
- 会开启一个前台服务,在后台进行Dump操作【注意它是个耗时行为】
- Dump内存的设计思路是怎样的?
- 1、分析堆快照;2、发送分析进度事件;3、发送分析完成事件。
- Dump内存如何设计触发堆快照,生成hprof文件
- 如果内存真的泄露,则发起 heap dump,分析 dump 文件,找到引用链。但是是每次泄漏都会触发该操作吗?
- 第一种策略:如果两次Dump之间时间少于60s,也会直接返回,避免频繁Dump。
- 第二种策略:默认情况下,如果泄漏对象计数未达到阈值,不会进行Dump,节省资源
2.7 设计分析内存快照
2.9 KOOM设计思路
- Koom加快Dump速度的设计思路
- LeakCanary 默认的 Java Heap Dump 使用的是 Debug.dumpHprofData() ,在 Dump 的过程中会有较长时间的应用冻结时间。
- 快手技术团队在开源框架 Koom 中提出了优化方案:利用 Copy-on-Write 思想,fork 子进程再进行 Heap Dump 操作。
03.LeakCanary原理
3.1 思考一些问题
- 首先思考几个问题
- LeakCanary:LeakCanary 的核心工作流程,它的设计思想是什么?初始化做了那些工作?如何设计组件的监听(怎么监听ViewModel,Service销毁事件)?
- LeakCanary:如何判断内存的泄漏?内存泄漏的判定条件是什么 ?检测内存泄漏的机制原理是什么?发现泄漏对象后就会触发分析吗?
- LeakCanary:检测出内存泄漏后,它又是如何生成泄漏信息的? 内存泄漏的输出轨迹是怎么得到的?为什么不能用到线上环境?
- 原理简单介绍
- 通过监听Activity的onDestroy,手动调用GC,然后通过ReferenceQueue+WeakReference,来判断Activity对象是否被回收,然后结合dump Heap的hprof文件,通过Haha开源库分析泄露的位置。
3.2 原理流程概括
- 如何触发检测
- 检测保留的实例 LeakCanary 是基于 LeakSentry 开发的,LeakSentry 会 hook Android 声明周期,并且会自动检测当 Activity 或 Fragment 被销毁时,它们的实例是否被回收了。
- 销毁的实例会传给 RefWatcher,RefWatcher 会持有它们的弱引用。 你也可以观察所有不再需要的实例,比如一个不再使用的 View,不再使用的 Presenter 等。
- 如果等待了 5 秒,并且 GC 触发了之后,弱引用还没有被清理,那么 RefWatcher 观察的实例就可能处于内存泄漏状态了。
- 判断是否存在内存泄漏
- 首先尝试着从ReferenceQueue队列中获取待分析对象(软引用和弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用或弱引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用或弱引用加入到与之关联的引用队列中),如果不为空,那么说明正在被系统回收。
- 如果直接就返回DONE,说明已经被系统回收了。如果没有被系统回收,可能存在内存泄漏,手动触发系统GC,然后再尝试移除待分析对象。如果还存在,说明存在内存泄漏。
- 分析内存泄漏
- 确定有内存泄漏后,调用heapDumper.dumpHeap()生成.hprof文件目录。HAHA 是一个由 square 开源的Android 堆分析库,分析hprof文件生成Snapshot对象。Snapshot用以查询对象的最短引用链。找到最短引用链后,定位问题,排查代码将会事半功倍。
image - 泄漏分组 当有两个泄漏分析结果相同时,LeakCanary 会根据子引用链来判断它们是否是同一个原因导致的,如果是的话,LeakCanary 会把它们归为同一组,以免重复显示同样的泄漏信息。
- 整体大概的流程
image
3.3 初始化流程
3.4 监听组件销毁
- 思考一个问题:各组件的内存泄漏监听方案是怎样设计的呢?
- Activity(ActivityWatcher):内部注册了一个 Activity 的全局生命周期监听,从而在 onDestroy() 时去追踪当前 activity 对象是否内存泄漏。
- Fragment(FragmentAndViewModelWatcher):先注册 Act-Lifecycle 监听,然后在 onCreate() 时进行 Fragment-Lifecycle 注册监听,并在 onFragmentViewDestroyed() 与 onFragmentDestroyed() 对 view 对象与 fragment 对象进行了内存泄漏追踪。
- 如何实现 ViewModel#onClear() 全局监听操作?
- 在 Activity#onCreate 和 Fragment#onCreate 事件中实例化一个自定义ViewModel,在进入 ViewModel#onClear() 方法时,通过反射获取当前作用域中所有的 ViewModel 对象交给 ObjectWatcher 监控。
- 如何实现 Service#onDestroy() 全局监听的方法?
- 1、Hook 主线程消息循环的 mH.mCallback 回调,监听其中的 STOP_SERVICE 消息,将即将 Destroy 的 Service 对象暂存起来(由于 ActivityThread.H 中没有 DESTROY_SERVICE 消息,所以不能直接监听到 onDestroy() 事件,需要第 2 步);
- 2、使用动态代理 Hook AMS 与 App 通信的的 IActivityManager Binder 对象,代理其中的 serviceDoneExecuting() 方法,视为 Service#onDestroy() 的执行时机,拿到暂存的 Service 对象交给 Watcher 监控。
- 如何实现 View#remove() 全局移除监听的操作?
- 1、Hook WMS 服务内部的 WindowManagerGlobal.mViews RootView 列表,获取 RootView 新增和移除的时机;
- 2、检查 View 对应的 Window 类型,如果是 Dialog 或 DreamService 等类型,则在注册 View#addOnAttachStateChangeListener() 监听,在其中的 onViewDetachedFromWindow() 回调中将 View 对象交给 Watcher 监控。
3.5 监听无用对象
- 如何判断内存泄漏的情况
- 第一步:对于要监听的对象,使用 KeyedWeakReference 与其进行关联(初始化时传入了一个引用队列 queue),并将其保存到专门的观察 Map 中。
- 第二步:这样当该对象被 Gc 回收时,就会出现在相应的引用队列中。然后,使用 Handler 延迟 5s后再去判断是否存在内存泄漏。
- 在具体的判断逻辑中,会先将引用队列中出现的对象从要观察的 Map 中移除,从而避免误判。
- 然后再判断当前要观察的对象是否存在,如果不存在,则说明没有内存泄漏;否则意味着可能出现了内存泄漏,则调用 Runtme.getRunTime().gc() 进行 GC 通知。
- Gc之后并且等待 100ms 后再次执行判断,若该观察的对象仍然存在于观察者 Map 中,则证明该对象真的已经泄漏,此时就会根据内存泄漏的个数弹出通知或者开始 dump hprof 。
3.7 Dump内存快照
- 在判定有内存泄漏后
- LeakCanary调用将系统提供的Debug.dumpHprofData(File file)函数,改函数将生成一个虚拟机的内存快照,文件格式为 .hprof,这个dump文件大小通常有10+M。
- 生成dump文件后,LeakCanary 将被泄露对象的 referenceKey 与 dump 文件 对象封装在 HeapDump 对象中,然后交给ServiceHeapDumpListener处理,在ServiceHeapDumpListener中创建 leakcanary 进程并启动服务 HeapAnalyzerService。
3.8 分析堆快照
- LeakCanary如何分析hprof文件
- 分析hprof文件的工作主要是在HeapAnalyzerService类中完成的。https://juejin.cn/post/6968084138125590541/#heading-13
- 分析hprof文件的简单流程:
- 1.解析文件头信息,得到解析开始位置
- 2.根据头信息创建Hprof文件对象
- 3.构建内存索引
- 4.使用hprof对象和索引构建Graph对象
- 5.查找可能泄漏的对象与GCRoot间的引用链来判断是否存在泄漏(使用广度优先算法在Graph中查找)
04.一些技术点思考
4.5 引用链如何生成
- 泄漏对象的引用链是分析报告的核心信息,LeakCanary 会收集泄漏对象到 GC Root 的完整引用链信息。它如何生产的?
4.6 分析引擎的原理
4.7 提高Dump分析效率
- Dump分析是十分耗时的行为
- 由于 LeakCanary 分析堆快照的过程存在一定的内存消耗,整个分析过程一般会持续几十秒,对于一些性能差的机型会造成明显的卡顿甚至 ANR。
- 如何提高Dump分析效率
- 提供了对多进程的支持。单独开个进程,将分析工作转移到子进程中,这样就不会影响主进程。
4.8 相同问题分组
- LeakCanary 会将相同问题重复触发的内存泄漏进行分组
- 为了减少重复的排查工作,分组方法是按引用链的签名。引用链签名是对引用链上经过的每个对象的类型拼接后取哈希值,既然应用链完全相同,就没必要重复排查了。
- 那么对应的泄漏签名计算公式是如何设计的?
- 待完善
4.9 如何标记怀疑对象
- 为了提高排查内存泄漏的效率,会标记怀疑对象
- LeakCanary 会自动帮助我们根据对象的生命周期信息或状态信息缩小排查范围,排除原本就具有全局生命周期的对象,剩下的用 ~~~ 下划线标记为怀疑对象。
- 标记怀疑对象的策略是如何设计的?
- 待完善
06.其他设计说明
6.1 性能设计
- 为什么 LeakCanary 不能用于线上
- 每次内存泄漏以后,都会生成并解析 hprof 文件,容易引起手机卡顿等问题
- 多次调用 GC,可能会对线上性能产生影响
- hprof 文件较大,信息回捞成问题
- 了解了这些问题,我们可以尝试提出一些解决方案:
- 1.可以根据手机信息来设定一个内存阈值 M ,当已使用内存小于 M 时,如果此时有内存泄漏,只将泄漏对象的信息放入内存当中保存,不生成.hprof文件。当已使用大于 M 时,生成.hprof文件
- 2.当引用链路相同时,可根据实际情况去重。
- 3.不直接回捞.hprof文件,可以选择回捞分析的结果
- 4.可以尝试将已泄漏对象存储在数据库中,一个用户同一个泄漏只检测一次,减少对用户的影响
6.2 稳定性设计
6.3 灰度设计
6.4 降级设计
6.5 异常设计优化
通过ReferenceQueue+WeakReference,来判断对象是否被回收
- WeakReference创建时,可以传入一个ReferenceQueue对象,假如WeakReference中引用对象被回收,那么就会把WeakReference对象添加到ReferenceQueue中,可以通过ReferenceQueue中是否为空来判断,被引用对象是否被回收
手动调用GC后还调用了System.runFinalization();,这个是强制调用已失去引用对象的finalize方法
- 在可达性算法中,不可达对象,也不是非死不可,这时他们处于“缓刑”阶段,要宣告一个对象真正死亡需要至少俩个标记阶段, 如果发现对象没有引用链,则会进行第一次标记,并进行一次筛选,筛选的条件是此对象是否有必要进行finalize()方法,当对象没有覆盖finalize(),或者finalize()已经调用过,这俩种都视为“没有必要执行”
参考博客
- 为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为是真强啊!
- https://zhuanlan.zhihu.com/p/556718460
- https://www.cnblogs.com/pengxurui/p/16614804.html
- 一文让你彻底理解LeakCanary的工作原理
- https://mp.weixin.qq.com/s/UfxG41HInNfv9nkDvKpcZQ
- 「Leakcanary 源码分析」看这一篇就够了
- https://mp.weixin.qq.com/s/n3_Zoc1UgG3Wzqv3ZrGMzA