性能优化专题十二--LeakCanary简析

简介

LeakCanary是一款开源的内存泄漏检查工具,在项目中,可以使用它来检测Activity是否能够被GC及时回收。github的地址为https://github.com/square/leakcanary

LeakCanary的核心原理是基于WeakReference和ReferenceQueue进行检测。WeakReference的构造函数可以传入ReferenceQueue,当WeakReference指向的对象被垃圾回收时,会把WeakReference放入ReferenceQueue。调用ReferenceQueue.poll()可以把WeakReference获取出来。

LeakCanary使用:

引入库:

  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 被销毁并且应该被垃圾回收时候自动检测。这些被销毁的对象被传递给RefWatcherRefWatcher持有这些被销毁对象的弱引用(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<String>:集合持有待检测的以及已经泄露的对象的key;

ReferenceQueue<Object>:引用队列,用于判断弱引用所持有的对象是否已经执行了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,然后做第二次判断,如果还是不存在,则表明出现了内存泄漏。

猜你喜欢

转载自blog.csdn.net/cpcpcp123/article/details/106970343