1 序
基于com.squareup.leakcanary:leakcanary-android:2.5
LeakCanary 是一个适用于 Android 的内存泄漏检测库,其具有一种独特的能力,可以缩小每个泄漏的原因,从而帮助开发人员显着减少OutOfMemoryError崩溃。
LeakCanary 自动检测以下对象的泄漏:
- 销毁的Activity实例
- 销毁的Fragment实例
- 销毁的片段View实例
- 清除ViewModel实例
那么LeakCanary是通过什么方式来检查内存泄漏的呢?
2 LeakCanary检测内存泄漏的原理
在分析LeakCanary检测内存泄漏的原理之前我们先来看下Java中存在的4种引用类型:
2.1 Java引用分类
- 强引用:平时常用的引用类型,JVM发生OOM也不会回收这部分引用。
- 软引用(SoftReference):对于软引用关联着的对象,在JVM应用即将发生内存溢出异常之前,将会把这些软引用关联的对象列进去回收对象范围之中进行第二次回收。如果这次回收之后还是没有足够的内存,才会抛出内存溢出异常。
- 弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集发生之前,简言之就是:一旦发生GC必定回收被弱引用关联的对象,不管当前的内存是否足够。也就是弱引用只能活到下次GC之时。
- 虚引用(PhantomReference):一个对象是否关联到虚引用,完全不会影响该对象的生命周期,也无法通过虚引用来获取一个对象的实例(PhantomReference覆盖了Reference#get()并且总是返回null)。为对象设置一个虚引用的唯一目的是:能在此对象被垃圾收集器回收的时候收到一个系统通知。
2.2 Refercence及ReferenceQueue:
1 | |
referent代表这个引用关联的对象queue引用队列。如果引用关联的对象即将被垃圾回收器回收,那个该对象会被添加到这个队列中。在关联对象时可以不指定队列,那么queue的值就是-ReferenceQueue.NULL,后续在入队过程中,检测到当前引用拥有的时这个队列,会直接返回false。next引用链表中的下一个元素。虽然引用有引用队列,但是引用是通过这个来形成单向链表的,并不依赖ReferenceQueue,ReferenceQueue中只保存了这个链表的head节点。discovered基于状态表示不同链表中的下一个待处理的对象,主要是pending-reference列表的下一个元素,通过JVM直接调用赋值。
对于这些引用的处理,在java后台会有一个专门的守护线程ReferenceHandler来执行,我们这儿不做深究。
2.3 LeakCanary检测内存泄漏的原理
通过上面对java引用类型和Refercence及ReferenceQueue的了解,我们可以知道:如果我们用一个
弱引用WeakReference来保存对象,并且在实例化WeakReference时传入一个ReferenceQueue,那么我们在调用系统gc方法时这个对象就会被放入ReferenceQueue中等待系统回收,我们此时遍历ReferenceQueue:
- 如果内部包含我们之前保存的对象,那么是不是可以认为这个对象已经被系统回收而没有出现内存泄漏。
- 如果内部未包含我们之前保存的对象,那么是不是可以认为已经发生了内存泄漏。
没错,这就是LeakCanary检测内存泄漏的核心原理,LeakCanary内部用到了Refercence及ReferenceQueue来实现对对象是否被回收的监听。
接下来我们通过分析源码来看一看LeakCanary是如何检测内存泄漏,并发出内存泄漏通知的。
3 LeakCanary源码解析
为了更好的对LeakCanary源码进行分部解析,我们先对LeakCanary实现内存泄漏的整体过程做一个概括:
- 初始化。
- 添加相关监听对象销毁监听,LeakCanary会默认监听Activity、Fragment、Fragment的View、ViewModel是否回收。
- 收到销毁回调后,根据要回收对象创建KeyedWeakReference和ReferenceQueue,并关联。
- 延迟5秒检查相关对象是否被回收。
- 如果没有被回收就通过dump heap获取hprof文件。
- 通过Shark库解析hprof文件,获取泄漏对象,被计算泄漏对象到GC roots的最短路径。
- 合并多个泄漏路径并输出分析结果。
- 将结果展示到可视化界面。
3.1 LeakCanary初始化
LeakCanary使用了ContentProvider来自动初始化。我们都知道ContentProvider的onCreate的调用时机介于Application的attachBaseContext和onCreate之间,Provider的onCreate优先于Application的onCreate执行。此时的Application已经创建成功,而Provider里的context正是Application的对象。目前很多三方库都采用这种无感知的初始化方式了。
1 | |
我们可以看到AppWatcherInstaller初始话内部调用了AppWatcher.manualInstall(application),最终内部调用的是InternalAppWatcher.install(application)完成初始化:
1 | |
3.2 观察是否发生内存泄漏
我们以activity为例来探究LeakCanary是如何进行内存泄漏检测的实现的。我们接着来看ActivityDestroyWatcher.install()方法:
1 | |
可以看到ActivityDestroyWatcher.install()内部实际上是注册了一个ActivityLifecycle监听,在onActivityDestroyed回调中调用了objectWatcher.watch()方法来观察当前activity是否发生了内存泄漏。
接着我们来看下objectWatcher.watch()内部都干了什么:
1 | |
调用removeWeaklyReachableObjects()清空queue,即移除之前已回收的引用。
我们来接着看:
1 | |
这个方法的作用就是判断哪些对象已经被加入ReferenceQueuee等待回收了,然后从本地watchedObjects中将这些对象移除掉。
第一次调用是清除之前的已回收对象,后面在moveToRetained中还会再次调用该方法判断引用是否正常回收。
我们接着来看下checkRetainedExecutor.execute { moveToRetained(key) }方法,执行一个延时任务,5秒后判断引用对象是否未被回收。为什么是5秒,我们前面在介绍AppWatcherInstaller类的时候已经说过。
1 | |
在activity destroy 5s后,我们走到了moveToRetained方法,先是调用removeWeaklyReachableObjects()方法将等待回收的对象从watchedObjects中移除,然后通过指定的key查找对应的KeyedWeakReference(这个KeyedWeakReference对应的就是我们activity销毁时,Activity对象封装成KeyedWeakReference)是否在watchedObjects还存在。
- 存在:可能发生了泄漏,通知注册了的Listener.
- 不存在:nothing
3.3 发起GC再次检测
上面说到发现内存泄漏后会通知注册了的Listener,这个Listener实际上就是InternalLeakCanary:
1 | |
可以看到,内部实际上是调用了heapDumpTrigger.scheduleRetainedObjectCheck()方法:
1 | |
dumpHeap() 获取内存快照,生成hprof文件,来看下dumpHeap方法实现:
1 | |
3.4 hprof文件解析
在上面讲到的内存泄漏回调处理中,生成了hprof文件,并开启一个服务来解析该文件。在Service的onHandleIntentInForeground回调方法中进行hprof文件解析:
1 | |
调用链:HeapAnalyzerService.analyzeHeap–>HeapAnalyzer.analyze。该方法实现了解析hprof文件找到内存泄漏对象,并计算对象到GC roots的最短路径,输出报告。
1 | |
接着我们来看下FindLeakInput.analyzeGraph方法查找内存泄漏对象:
1 | |
最终在可视化界面中将hprof分析结果HeapAnalysisSuccess展示出来。
3.5 将内存泄漏信息通知用户
onHeapAnalyzedListener.onHeapAnalyzed的实现类是DefaultOnHeapAnalyzedListener,来看下具体实现:
1 | |
4 总结
我们以Activity为例,来总结下LeakCanary检测内存泄漏过程:
- 监听
Activity的onDestroy事件 - 在
onDestroy事件回调中使用objectWatcher.watch()创建引用activity的KeyedWeakReference对象,并关联ReferenceQueue。 - 延时5秒调用
moveToRetained()检查目标对象是否回收。 - 未回收则手动触发
gc再次检测未回收对象数量。 - 未回收对象大于5个则开启服务,dump heap获取内存快照hprof文件。
- 解析hprof文件根据
KeyedWeakReference类型过滤找到内存泄漏对象。 - 计算对象到GC roots的最短路径,并合并所有最短路径为树结构。
- 输出分析结果,并根据分析结果展示到可视化页面。
