LeakCanary源码分析

LeakCanary是Android中用于排查Activity、Fragment内存泄漏的工具。
本文基于LeakCanary 1.6.1源码分析。

1
2
3
4
5
LeakCanary.install(this);

public static RefWatcher install(Application application) {
return ((AndroidRefWatcherBuilder)refWatcher(application).listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build())).buildAndInstall(); // 通过AndroidRefWatcherBuilder.buildAndInstall()创建RefWatcher。
}
  • DisplayLeakService和DisplayLeakActivity用于确定分析Heap内存信息并将泄漏信息显示。
  • AndroidExcludedRefs记录了Android源码中内存泄漏的地方,如果项目中出现也会提示,并添加[Excluded]标志。
  • 通过ActivityRefWatcher监控Activity内存泄漏。
  • 通过FragmentRefWatcher监控Fragment内存泄漏。

接下来看看ActivityRefWatcher如何分析Acitivty内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.refWatcher.watch(activity);
}
};

public static void install(Context context, RefWatcher refWatcher) {
Application application = (Application)context.getApplicationContext();
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}

ActivityRefWatcher利用registerActivityLifecycleCallbacks监控Activity生命周期,注册了一个ActivityLifecycleCallbacks。
ActivityLifecycleCallbacks在Activity的onDestroy时使用RefWatcher进行watch。

接下来进入RefWatcher看看watch方法。

1
2
3
4
5
6
7
8
9
10
11
public void watch(Object watchedReference, String referenceName) {
...
// watchedReference即activity引用,创建一个key对应该activity并存入retainedKeys
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
// 生成一个WeakReference弱引用,这里还会使用到一个ReferenceQueue
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
// 通过一个watchExecutor异步执行ensureGone判断内存泄漏情况
ensureGoneAsync(watchStartNanoTime, reference);
}

retainedKeys是一个Set,通过UUID生成一个key与Activity对应。retainedKeys存放的是使用中的引用(即Activity)key值。
ReferenceQueue,用来监听GC后的回收情况,在Acitivity.onDestroy回收成功的Activity引用会存放进这个队列。反过来说,即ReferenceQueue存放的都是释放成功的Activity引用,从ReferenceQueue取出的值都表示该Activity已被正确回收。
gc后弱引用被回收,retainedKeys中还存在有引用,则代表可能发生了内存泄漏。

接下来看看ensureGone方法,这也是LeakCanary的核心原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
...
// 移除弱引用
removeWeaklyReachableReferences();
...
// 立即进行一次gc(System.gc无法立即进行gc,这里参考了AOSP源码的实现使用Runtime.gc
,并Thread.sleep 100ms,用于尽可能唤起系统进行gc)
gcTrigger.runGc();
// 移除弱引用
removeWeaklyReachableReferences();
// 判断retainedKeys是否还包含该引用,包含则说明可能存在内存泄漏
if (!gone(reference)) {
// dumpHeap
File heapDumpFile = heapDumper.dumpHeap();
}
// 通过dumpHeap分析内存情况
...
}
return *DONE*;
}

removeWeaklyReachableReferences则用于判断Activity是否进行了正确的内存回收。

1
2
3
4
5
6
7
8
private void removeWeaklyReachableReferences() {
// queue即ReferenceQueue,从其poll出来的引用代表内存被正确回收
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
// 所以内存被正确回收的Activity从retainedKeys中移除
retainedKeys.remove(ref.key);
}
}

这里为何要进行两次gc,为何不是一次,也不是三四次?

看一下官方对弱引用的定义:
假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后把这个弱可达对象标记为可终结(finalizable)的,这样它随后就会被回收。与此同时或稍后,垃圾收集器会把那些刚清除的弱引用放入创建弱引用对象时所指定的引用队列(Reference Queue)中。

可能存在被观察的引用将要变得弱可达,但是还未入队引用队列,这个时候可能会错误判断这个对象未被回收,所以这时候应该再主动调用一次 GC,可能可以避免一次heap dump。频繁的heap dump会卡住应用。

至此,LeakCanary的核心原理已经分析完毕。
后续利用 heapDumper 把内存情况 dump 成文件,并调用 heapdumpListener 进行内存分析,进一步确认是否发生内存泄漏。

gc是会耗费时间的,影响主线程性能,那么LeakCanary是怎么做处理的。
Android的Handler、Looper机制中,主线程Looper在循环从MessageQueue中取消息时,会执行MQ的next方法,Android在MQ里预留了mIdleHandlers,用于处理空闲任务。即主线程没有消息处理时。
LeakCanary在RefWatch的ensureGoneAsync方法,会利用到mIdleHandlers。

1
2
3
4
5
6
7
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
return ensureGone(reference, watchStartNanoTime);
}
});
}

AndroidWatchExecutor的execute方法如下。

1
2
3
4
5
6
7
8
9
public void execute(Retryable retryable) {
// 判断是否是主线程
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
this.waitForIdle(retryable, 0);
} else {
// 不是主线程则MainHandler进行post
this.postWaitForIdle(retryable, 0);
}
}

两者最终都是通过waitForIdle往MainHandler添加一个IdleHandler。

1
2
3
4
5
6
7
8
private void waitForIdle(final Retryable retryable, final int failedAttempts) {
Looper.myQueue().addIdleHandler(new IdleHandler() {
public boolean queueIdle() {
AndroidWatchExecutor.this.postToBackgroundWithDelay(retryable, failedAttempts);
return false;
}
});
}

最后通过postToBackgroundWithDelay方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
// 通过某种规则计算延迟时间
long exponentialBackoffFactor = (long)Math.min(Math.pow(2.0D, (double)failedAttempts), (double)this.maxBackoffFactor);
long delayMillis = this.initialDelayMillis * exponentialBackoffFactor;
this.backgroundHandler.postDelayed(new Runnable() {
public void run() {
// 这里就是执行上述实现的ensureGone任务
Result result = retryable.run();
// 判断是否要进行重试
if (result == Result.RETRY) {
AndroidWatchExecutor.this.postWaitForIdle(retryable, failedAttempts + 1);
}
}
}, delayMillis);
}

最近又查看了LeakCanary 2.0版本的代码,使用了kotlin进行改写,但是核心流程是一样的。
LeakCanary2不需要开发者在Application类手动调用install。LeakCanary2通过Manifest注册一个Provider,实现Provider的方法并在onCreate获取Application的Context,由于Provider的onCreate生命周期在Application的onCreate之前,于是无需在Application的onCreate进行注册。

最后总结一下:

  1. 在Application中LeakCanary.install时,通过AndroidRefWatcherBuilder创建RefWatch用于监控内存泄漏情况。
  2. ActivityRefWatcher利用registerActivityLifecycleCallbacks监控Activity生命周期,在Activity的onDestroy时使用RefWatcher进行watch。
  3. LeakCanary利用MessageQueue中的IdleHandler,在UI空闲时间处理gc和内存泄漏监控,防止影响UI性能。
  4. 检查内存泄漏的核心是利用WeakReference包装Activity,使用一个ReferenceQueue来记录该 WeakReference 指向的对象是否已被回收。当系统回收或者手动触发gc回收Acitivty,成功回收的Activity的WeakReference会加入ReferenceQueue,ReferenceQueue取出的即使正确被回收的Activity,retainedKeys留下记录未被回收的Activity的key。
  5. 通过dump出Heap文件,然后通过分析Heap文件的内存情况,最终通过DisplayLeakService和DisplayLeakActivity将内存泄漏信息通知和显示。

参考文章
http://wingjay.com/2017/05/14/dig_into_leakcanary/
Java内存问题 及 LeakCanary 原理分析 - 掘金
https://juejin.im/post/5d1225546fb9a07ecd3d6b71

0%