LeakCanary是一款开源的内存泄漏检查工具,在项目中,可以使用它来检测Activity是否能够被GC及时回收。github的地址为https://github.com/square/leakcanary
LeakCanary的核心原理是基于WeakReference和ReferenceQueue进行检测。WeakReference的构造函数可以传入ReferenceQueue,当WeakReference指向的对象被垃圾回收时,会把WeakReference放入ReferenceQueue。调用ReferenceQueue.poll()可以把WeakReference获取出来。
引入库:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
// Optional, if you use support library fragments:
debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'
初始化:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);
}
}
LeakCanary原理简述:
1、Activity Destory之后将它放在一个WeakReference弱引用中;
2、将这个WeakReference关联到一个ReferenceQueue引用队列中
3、查看ReferenceQueue引用队列中是否存在Activity的引用
4、如果该Activity泄露了,Dump出Heap堆信息,然后再去分析泄露路径。
基础概念四种引用类型之间的区别详见性能优化专题二--内存优化(虚引用和弱引用的区别、枚举优化、对象池)
其中软引用、弱引用有一个共同点就是可以和ReferenceQueue联合使用,软引用所使用的对象如果被垃圾回收器回收了,那么java虚拟机就会把这个引用对象加入到相关联的引用队列中;弱引用同样的如果被垃圾回收器回收了,那么java虚拟机就会把这个弱引用加入到相关联的引用队列中。
LeakCanary实现简述LeakCanary的基础是一个叫做LeakCanary-watcher的library。它hook了Android的生命周期,当activity和fragment 被销毁并且应该被垃圾回收时候自动检测。这些被销毁的对象被传递给RefWatcher
, RefWatcher
持有这些被销毁对象的弱引用(weak references)。如果弱引用在等待5秒钟并运行垃圾收集器后仍未被清除,那么被观察的对象就被认为是保留的(retained
,在生命周期结束后仍然保留),并存在潜在的泄漏。LeakCanary会在Logcat中输出这些日志。
1、LeakCanary.install()返回了一个RefWatcher,启动一个ActivityRefWatcher,用于监视Activity的回收情况,通过ActivityLifecycleCallbacks把Activity的onDestory生命周期关联。
2、RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象,KeyedWeakReference继承自WeakReference。
3、在RefWatcher.watch()时把UUID放入RefWatcher.retainedKeys,并把对象跟RefWatcher.queue关联。
4、然后在后台线程检查引用是否被清除,如果没有,则通过Runtime.gc()和System.runFinalization()进行GC操作。
5、在RefWatcher.removeWeaklyReachableReferences()调用queue.poll()取出KeyedWeakReference,并从retainedKeys中删除。所以,如果retainedKeys中key仍存在,说明对象未被垃圾回收。反之则已经垃圾回收。
6、如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
7、在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
8、得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
9、HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
10、引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。
源码解析Install方法如下:
install方法会给我们返回一个RefWatcher类,Refwatcher实际上是为了启动ActivityRefWatcher类,ActivityRefWatcher会在Activity的OnDestory的回调方法调用完之后去探测Activity的内存泄露。
其中listenerServiceClass方法传入了展示分析结果的Service(DisplayLeakService);
excludedRefs方法排除了开发中可以忽略的泄漏路径;
buildAndInstall是主要的函数,实现了activity是否能被释放的监听;
buildAndInstall核心有两处:
该方法的返回值会返回RefWatcher,用于启动Activity的RefWatcher,ActivityRefWatcher用于监听Activity的回收情况
通过build方法返回了RefWatcher对象,里面存储的是弱引用队列中未被回收的对象的引用;
- 核心一:build方法创建并初始化和RefWatcher相关的成员变量,如下所示:
/** Creates a {@link RefWatcher}. */
public final RefWatcher build() {
if (isDisabled()) {
return RefWatcher.DISABLED;
}
if (heapDumpBuilder.excludedRefs == null) {
heapDumpBuilder.excludedRefs(defaultExcludedRefs());
}
HeapDump.Listener heapDumpListener = this.heapDumpListener;
if (heapDumpListener == null) {
heapDumpListener = defaultHeapDumpListener();
}
DebuggerControl debuggerControl = this.debuggerControl;
if (debuggerControl == null) {
debuggerControl = defaultDebuggerControl();
}
HeapDumper heapDumper = this.heapDumper;
if (heapDumper == null) {
heapDumper = defaultHeapDumper();
}
WatchExecutor watchExecutor = this.watchExecutor;
if (watchExecutor == null) {
watchExecutor = defaultWatchExecutor();
}
GcTrigger gcTrigger = this.gcTrigger;
if (gcTrigger == null) {
gcTrigger = defaultGcTrigger();
}
if (heapDumpBuilder.reachabilityInspectorClasses == null) {
heapDumpBuilder.reachabilityInspectorClasses(defaultReachabilityInspectorClasses());
}
return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,
heapDumpBuilder);
}
RefWatcher中的成员变量:
WatchExecutor:用于执行内存泄露的检测;
DebugControl:查询是否正在调试中,如果代码正在调试中,那么leakcanary就不会执行内存泄露的检测判断;
GcTrigger:用于处理GC,在判断内存泄露之前,会给这个对象一次机会,调用GC进行回收,如果没有回收成功则会dump出内存文件给用户;
HeapDumper:dump出内存泄露的堆文件;
Set:集合持有待检测的以及已经泄露的对象的key;
ReferenceQueue:引用队列,用于判断弱引用所持有的对象是否已经执行了GC垃圾回收;
HeapDump.Listener:用于分析一些产生heap文件的回调;
- 核心二:ActivityRefWater.install方法就是将Activity注册到这个watcher监控队列中:
通过registerActivityLifecycleCallbacks来监听Activity的生命周期:
lifecycleCallbacks监听Activity的onDestroy方法,正常情况下activity在onDestroy后需要立即被回收,onActivityDestroyed方法最终会调用RefWatcher.watch方法:
这里再次强调下,弱引用和引用队列ReferenceQueue联合使用时,如果弱引用持有的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。即 KeyedWeakReference持有的Activity对象如果被垃圾回收,该对象就会加入到引用队列queue。监测机制利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收。检查方法如下:
ensureGone():是为了确保Activity是确实被回收掉了,为什么要确保Activity已经经历过了GC回收呢?
因为在dump内存信息之前提示内存泄漏时,我们希望系统已经经过充分的GC垃圾回收了,而不要出现任何的误判。
removeWeaklyReachableReferences():清除已经到达引用队列的弱引用,把已经回收掉的key从应用集合中移除,那么剩下的就可以保证是未被回收的泄露的对象
debuggerControl.isDebuggerAttached():如果是debug状态,那么就不会执行内存泄露的分析。
gone(reference):如果该引用已经回收掉了,那么就返回Result.DONE,因为对象已经回收掉了,则没有泄露。
再往下面才真正进入GC的回收,
gcTrigger.runGc():如果当前对象还是可达状态,那么就调用GC进行垃圾回收,手动触发垃圾回收,然后再次调用removeWeaklyReachableReferences()清除已经到达引用队列的弱引用,最终如果这个引用还没被清除掉,
if (!gone(reference)):那么就需要进行引用链路的分析了。dump出内存信息heapdumpListener.analyze(heapDump)进行后续的分析。
下面我们来继续看下analyze这个方法:
在ServiceHeapDumpListener中:
并调用到HeapAnalyzerService中的onHandleIntent方法中:
其中HeapDump就是我们刚刚截取的堆文件,而HeapAnalzyer顾名思义,就是分析堆文件的
传入到HeapAnalzyer中的第一个参数是为了排除系统的内存泄露。下面的checkForLeak是真正分析内存泄露的地方,也是最为重要的方法。
首先将Hprof转化为Snapshot内存快照,其中Snapshot中包含了所有对象的引用路径,那么对Snapshot进行解析去除重复泄露路径,最终返回泄露对象、泄露对象的最短路径,输出最终检测结果。
onDestroy以后,一旦主线程空闲下来,延时5秒执行一个任务:先判断Activity有没有被回收?如果已经回收了,说明没有内存泄漏,如果还没回收,我们进一步确认,手动触发一下gc,然后再判断有没有回收,如果这次还没回收,说明Activity确实泄漏了,接下来把泄漏的信息展示给开发者就好了。
1、 首先通过removeWeaklyReachablereference来移除已经被回收的Activity引用
2、 通过gone(reference)判断当前弱引用对应的Activity是否已经被回收,如果已经回收说明activity能够被GC,直接返回即可。
3、 如果Activity没有被回收,调用GcTigger.runGc方法运行GC,GC完成后在运行第1步,然后运行第2步判断Activity是否被回收了,如果这时候还没有被回收,那就说明Activity可能已经泄露。
4、 如果Activity泄露了,就抓取内存dump文件(Debug.dumpHprofData)
5、 之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析
6、获取到heap文件后,通知ServiceHeapDumpListener进行分析heapdumpListener.analyze(heapDump);
7、接着通过HeapAnalyzer(checkForLeak—findLeakingReference---findLeakTrace)来进行内存泄漏分析。
8、 最后通过DisplayLeakService进行内存泄漏的展示。
9、 在堆转储中搜索具有相应键的{@link KeyedWeakReference}实例,然后计算从该实例到GC根的最短强引用路径
findLeakingReference:如何找到内存泄露的引用;
findLeakTrace:如何找到泄露的最短路径。
首先看下findLeakingReference方法:
通过查找弱引用找到泄露对象。
再来看findLeakTrace这个方法:
其中ShortestPathFinder就是查找GCRoots,我们重点关注的GCROOT类型是java静态变量以及在线程中正在使用的对象线程仍在执行中,最终通过buildLeakTrace来创建内存泄露的调用栈,最终展示在屏幕上的就是这个leaktrace.
总结LeakCanary实现内存泄漏的主要判断逻辑是这样的。当我们观察的Activity或者Fragment销毁时,我们会使用一个弱引用去包装当前销毁的Activity或者Fragment,并且将它与本地的一个ReferenceQueue队列关联。我们知道如果GC触发了,系统会将当前的引用对象存入队列中。 如果没有被回收,队列中则没有当前的引用对象。所以LeakCanary会去判断,ReferenceQueue是否有当前观察的Activity或者Fragment的引用对象,第一次判断如果不存在,就去手动触发一次GC,然后做第二次判断,如果还是不存在,则表明出现了内存泄漏。