LeakCanary监控原理解析

一、引言

最近项目中需要对App的性能进行监控并采集性能数据,在内存泄漏的监控上,采用了LeakCanary的Sdk。
LeakCanary是业界公认的比较好的一个内存监控项目,很早以前就听说并使用过,但是一直没有去了解其背后的监控原理。今天就借这个项目的开发之际,好好的分析一下其原理。

二、原理分析

我们还是从LeakCanary的使用上入手,代码如下:

final RefWatcher refWatcher = LeakCanary.install(this);
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
     ...省略未实现的方法
    @Override
    public void onActivityDestroyed(Activity activity) {
        refWatcher.watch(activity);
    }
});

上面的代码中使用了LeakCanary.install得到了一个RefWatcher对象,我们进入到LeakCanary的install方法,代码如下:

public static @NonNull RefWatcher install(@NonNull Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
    .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
    .buildAndInstalle();
}

这里使用了建造者模式,其中的listenerServiceClass方法设置了内存泄漏处理的ServiceexcludedRefs方法设置了不对哪些对象进行监听(这里可以设置Android系统的对象和第三方Sdk的对象),最后执行了buildAndInstalle方法生成了一个RefWatcher对象

在生成了RefWatcher对象后,需要就去监听Activity是否存在内存泄漏。这里我们使用了registerActivityLifecycleCallbacks方法,并在ActivityLifecycleCallbacks接口的onActivityDestroyed方法中监听Activity对象,代码如下:

registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
     ...省略未实现的方法
    @Override
    public void onActivityDestroyed(Activity activity) {
        refWatcher.watch(activity);
    }
});

可以看到,这里又调用了RefWatcher对象的watch方法,代码如下:

public void watch(Object watchedReference) {
    this.watch(watchedReference, "");
}

watch方法又调用了watch的重载方法,代码如下:

public void watch(Object watchedReference, String referenceName) {
    if (this != DISABLED) {
        Preconditions.checkNotNull(watchedReference, "watchedReference");
        Preconditions.checkNotNull(referenceName, "referenceName");
        long watchStartNanoTime = System.nanoTime();
        String key = UUID.randomUUID().toString();
        this.retainedKeys.add(key);
        KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
        this.ensureGoneAsync(watchStartNanoTime, reference);
    }
}

上面的代码判断了watchedReference对象和referenceName对象不为空,然后使用UUID.randomUUID().toString()创建了一个全局唯一key值。

对于UUID.randomUUID有疑问的可以看一下UUID.randomUUID()简单介绍 这篇文章

然后new了一个KeyedWeakReference对象,并将watchedReference对象全局唯一的key值引用对象的名字(这里为空)ReferenceQueue对象传入到构造函数中。
而LeakCanary监控内存泄漏的关键就在于KeyedWeakReference这个对象。所以,我们进入它的构造函数中看一下,代码如下:

KeyedWeakReference(Object referent, String key, String name, ReferenceQueue referenceQueue) {
    super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
    this.key = (String)Preconditions.checkNotNull(key, "key");
    this.name = (String)Preconditions.checkNotNull(name, "name");
}
 
 

KeyedWeakReference的构造函数简单的调用了一下其父类WeakReference的构造方法,而其父类WeakReference又调用了其父类的Reference的构造方法。最终是将watchedReference对象和ReferenceQueue对象分别赋值给了Reference的reference和queue

1、reference:
表示引用的对象,也就是上述我们所说的被监控对象。源码对其的描述为/* Treated specially by GC */,表示该对象会被对象特殊处理。而对象被回收后,Reference对象的queue队列会将自己(this对象)进行入队操作。

2、queue:
当该对象中有值时,表示引用的对象reference已经被GC回收。根据这个原理,我们就可以判断那些本应该被回收的对象如果没有被回收,就判断为内存泄漏

好了,让我们从KeyedWeakReference的构造函数回退到上面的watch方法,看看LeakCanary是怎么利用这个KeyedWeakReference的。watch方法最后调用了ensureGoneAsync方法,代码如下:

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    this.watchExecutor.execute(new Retryable() {
        public Result run() {
            return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
        }
    });
}

ensureGoneAsync方法在线程池中执行了ensureGone方法,代码如下:

Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    this.removeWeaklyReachableReferences();
    if (this.debuggerControl.isDebuggerAttached()) {
        return Result.RETRY;
    } else if (this.gone(reference)) {
        return Result.DONE;
    } else {
        this.gcTrigger.runGc();
        this.removeWeaklyReachableReferences();
        if (!this.gone(reference)) {
            long startDumpHeap = System.nanoTime();
            long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
            File heapDumpFile = this.heapDumper.dumpHeap();
            if (heapDumpFile == HeapDumper.RETRY_LATER) {
                return Result.RETRY;
            }

            long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
            HeapDump heapDump = this.heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key).referenceName(reference.name).watchDurationMs(watchDurationMs).gcDurationMs(gcDurationMs).heapDumpDurationMs(heapDumpDurationMs).build();
            this.heapdumpListener.analyze(heapDump);
        }

        return Result.DONE;
    }
}

ensureGone方法执行了下面几个步骤:
1、首先调用了removeWeaklyReachableReferences方法,代码如下:

private void removeWeaklyReachableReferences() {
    KeyedWeakReference ref;
    while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
        this.retainedKeys.remove(ref.key);
    }
}

该方法从queue进行出队列操作,queue.poll() != null 表明该引用对象被GC正常回收了,然后将该对象的标记key从retainedKeys中移除

2、然后判断当前是否处于debug状态,如果是则返回,否则第一次执行gone方法,gone方法的代码如下:

private boolean gone(KeyedWeakReference reference) {
    return !this.retainedKeys.contains(reference.key);
}

逻辑很简单,就是判断一下retainedKeys是否包含引用对象的标记key。如果不包含则表明,该对象被GC正常回收了,因此gone方法返回true,否则存在可疑的内存泄漏则返回false

3、如果第2步返回为false,LeakCanary不会马上触发内存泄漏告警,而是调用了GcTrigger对象的runGc方法主动告知GC,现在应该要回收对象了。

4、调用完GcTrigger对象的runGc方法后,接着又一次调用了removeWeaklyReachableReferences方法,看该对象是否被回收了。

5、然后第二次调用gone方法,如果此时的gone方法还是返回false,则表示该对象存在内存泄漏了。

6、接下来就是将当前时刻的内存堆栈dump下来,并在DisplayLeakService进行分析,然后在DisplayLeakActivity中进行展示。
而对于解析内存堆栈和展示内存泄漏信息的逻辑本文就不做分析了,因为这不属于本文的范畴。有兴趣的同学可以自己读一下源码。

三、总结

LeakCanary内存泄漏的监控原理是利用了Reference对象中的引用对象被回收时,会将其自己存储到ReferenceQueue队列,然后判断ReferenceQueue队列是否有值,来判断对象是否内存泄漏的。

而源码中的WeakHashMap同样的利用了这个原理判断引用的对象是否被回收了,后面有时间可以看一下WeakHashMap的源码进行验证一下。

你可能感兴趣的:(LeakCanary监控原理解析)