Android 开源库分析——LeakCanary分析

本文链接:https://rainmonth.github.io/posts/A200811.html
LeakCanary本质上就是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具。

注意:本文基于1.6.3版本进行分析。

内存泄漏检测的原理

主要就是利用检测对象的声明周期(ActivityFragment)回调中开启监听,依据弱引用在gc触发后会被加入到ReferenceQueue中,然后根据自己记录的要回收的对象key和已经加入到ReferenceQueue中的对象key做对比,如果自己记录的key中包含了ReferenceQueue中不存在的内容,则说明有内存泄漏发生了。

下面通过方法一步步跟踪来看看LeakCanary是如何实现内存泄漏检测的。

基本使用

调用install方法即可开启泄漏监听

1
2
3
4
5
6
7
@NonNull
public static RefWatcher install(@NonNull Application application) {
return ((AndroidRefWatcherBuilder)refWatcher(application)// 设置Application对象
.listenerServiceClass(DisplayLeakService.class)// 一个IntentService,用来分析内存泄漏
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())) // 指定Android要排除的已知的内存泄漏
.buildAndInstall();// 创建RefWatcher
}

buildAndInstall()代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@NonNull
public RefWatcher buildAndInstall() {
if (LeakCanaryInternals.installedRefWatcher != null) {
throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
} else {
RefWatcher refWatcher = this.build();
if (refWatcher != RefWatcher.DISABLED) { // 判断LeakCanary是否开启
if (this.enableDisplayLeakActivity) {
LeakCanaryInternals.setEnabledAsync(this.context, DisplayLeakActivity.class, true);
}

if (this.watchActivities) {// 是否监听Activity,默认true
ActivityRefWatcher.install(this.context, refWatcher);
}

if (this.watchFragments) {// 是否监听Fragment,默认true
Helper.install(this.context, refWatcher);
}
}

LeakCanaryInternals.installedRefWatcher = refWatcher;// 将RefWatcher赋值给LeakCanaryInternals.installedRefWatcher
return refWatcher;
}
}

有此可见,监听Activity是否泄漏发生在ActivityRefWatcher.install(this.context, refWatcher);,监听Fragment是否泄漏发生在Helper.install(this.context, refWatcher);,先看前者Activity内存泄漏的监听。

Activity 泄漏监听流程

ActivtyRefWatcher.java

1
2
3
4
5
6
7
public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
Application application = (Application)context.getApplicationContext();// 得到application context
// 创建ActivityRefWatcher,内部会初始化创建一个 ActivityLifecycleCallbacks 对象:lifecycleCallbacks
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
// 注册这个Callback,监听Activity的Lifecycle,主要是重写了 onActivityDestroyed(Activity activity)
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}

看代码:

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

很简单,在Activity的onDestroyed方法执行时,将destroy的Activity对象作为参数传递给RefWatcher的watch方法,看代码:

1
2
3
4
5
6
7
8
9
10
11
12
public void watch(Object watchedReference, String referenceName) {
if (this != DISABLED) {
Preconditions.checkNotNull(watchedReference, "watchedReference");// 确保Activity不为空
Preconditions.checkNotNull(referenceName, "referenceName");// 确保name不为null
long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();// 生成一个key
this.retainedKeys.add(key);// 将key添加到retainedKeys中,retainedKes是一个Set(Set是不允许有重复元素的)
// 将Activity对象包装厂 KeyedWeakReference,并将RefWatcher的ReferenceQueue传递进去
KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
this.ensureGoneAsync(watchStartNanoTime, reference);
}
}

ensureGoneAsync看名字就是一个异步方法,主要是通过AndroidWatchExcutor(内部开启了一个HandlerThread,可以通过HandlerThread获取Looper,然后通过改Looper构造一个Handler对象(子线程Handler)),调用连如下方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void execute(@NonNull Retryable retryable) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
this.waitForIdle(retryable, 0);
} else {
this.postWaitForIdle(retryable, 0);
}

}

private void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
this.mainHandler.post(new Runnable() {
public void run() {
AndroidWatchExecutor.this.waitForIdle(retryable, failedAttempts);
}
});
}

private void waitForIdle(final Retryable retryable, final int failedAttempts) {
Looper.myQueue().addIdleHandler(new IdleHandler() {
public boolean queueIdle() {
AndroidWatchExecutor.this.postToBackgroundWithDelay(retryable, failedAttempts);
return false;
}
});
}

本质就是想主线程消息队列中添加一个IdleHandler,当主线程空闲时从下午队列获取消息进行处理,处理是在AndroidWatchExecutor的HandlerThread线程进行的,见下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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() {
Result result = retryable.run();
if (result == Result.RETRY) {
AndroidWatchExecutor.this.postWaitForIdle(retryable, failedAttempts + 1);
}

}
}, delayMillis);
}

retryable.run(),调用的就是RefWatcher.this.ensureGone(reference, watchStartNanoTime);,现在看该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)) {// 确保引用对象(Activity)是否被回收,已经回收了,就完成了
return Result.DONE;
} else {
this.gcTrigger.runGc();// 触发GC
this.removeWeaklyReachableReferences();// 将弱引用队列队列中存在的(已经回收的)对象从retainKeys中移除
if (!this.gone(reference)) {// 再次判断是对象是否被回收,如果没有回收,进入if判断进行处理
long startDumpHeap = System.nanoTime();
long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = this.heapDumper.dumpHeap();// 获取到heapDump文件
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();// 创建HeapDump对象,交给
this.heapdumpListener.analyze(heapDump);// 调用 ServiceHeapDumpListener 进行分析。
}

return Result.DONE;
}
}

ServiceHeapDumpListener里面有一个Context和一个AbstractAnalysisResultService(实质上是一个IntentService,内部会开启一个HandlerThead,又是一个线程),看看analyze方法:

1
2
3
4
public void analyze(@NonNull HeapDump heapDump) {
Preconditions.checkNotNull(heapDump, "heapDump");
HeapAnalyzerService.runAnalysis(this.context, heapDump, this.listenerServiceClass);// 这个this.listenerServiceClass 指的是 DisplayLeakService
}

HeapAnalyzerService本身也是一个IntentService

1
2
3
4
5
6
7
8
public static void runAnalysis(Context context, HeapDump heapDump, Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
LeakCanaryInternals.setEnabledBlocking(context, HeapAnalyzerService.class, true);
LeakCanaryInternals.setEnabledBlocking(context, listenerServiceClass, true);
Intent intent = new Intent(context, HeapAnalyzerService.class);
intent.putExtra("listener_class_extra", listenerServiceClass.getName()); // 获取listenerServiceClass的名字,后面肯定是用Class.forName找到
intent.putExtra("heapdump_extra", heapDump);// 注意:这里直接将HeadDump数据通过Intent传递,
ContextCompat.startForegroundService(context, intent);
}

最终会调用 HeapAnalyzerService 的 onHandleIntentInForeground 方法:

1
2
3
4
5
6
7
8
9
10
11
protected void onHandleIntentInForeground(@Nullable Intent intent) {
if (intent == null) {
CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.", new Object[0]);
} else {
String listenerClassName = intent.getStringExtra("listener_class_extra");
HeapDump heapDump = (HeapDump)intent.getSerializableExtra("heapdump_extra");
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey, heapDump.computeRetainedHeapSize);
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
}

可见heapDump文件的分析工作最终是有HeapAnalyzer来完成的,其运行完checkForLeak方法后会得到 AnalysisResult 对象,最终交给 DisplayLeakService进行显示处理。其实就是将HeapDump文件和分析结果保存到文件中,将保存的文件路径传递给 DisplayLeakService,接下就是 DisplayLeakService的工作了。主要在它的onHeapAnalyzed中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected final void onHeapAnalyzed(@NonNull AnalyzedHeap analyzedHeap) {
HeapDump heapDump = analyzedHeap.heapDump;// 获取HeapDump信息
AnalysisResult result = analyzedHeap.result;// 获取分析结果
String leakInfo = LeakCanary.leakInfo(this, heapDump, result, true); // 获取leakInfo
CanaryLog.d("%s", new Object[]{leakInfo});// 输出泄漏log
heapDump = this.renameHeapdump(heapDump); // 重命名heapDump文件
boolean resultSaved = this.saveResult(heapDump, result);// 保存结果
if (resultSaved) {
// 结果保存完成后,创建一个PendingIntent,发送通知,最终会打开DisplayLeakActivity
PendingIntent pendingIntent = DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey);
String contentTitle;
String className;
...
className = this.getString(string.leak_canary_notification_message);
this.showNotification(pendingIntent, contentTitle, className);
} else {
this.onAnalysisResultFailure(this.getString(string.leak_canary_could_not_save_text));
}

this.afterDefaultHandling(heapDump, result, leakInfo);
}

其实到DisplayLeakActivity内存泄漏的分析工作就已经结束了,后面就是泄漏信息的展示了,这里不作赘述。

Fragment泄漏检测

有了上面Activity检测的经验,我才Fragment泄漏的检测原理应该也差不多,这里把整个流程快速过一遍。
FragmentRefWatcher.Helper.install(Context context, RefWatcher refWatcher)

  1. 负责添加FragmentRefWatcher
  2. 负责注册Activity生命周期回调,并在onActivityCreated中调用 FragmentRefWatcherwatchFragments(Activity activity)方法;
  3. watchFragments中根据Activity获取到对应的FragmentManager,然后通过manager注册FragmentLifecycleCallbacks回调;
  4. 在FragmentLifecycleCallbacks中,监听 onFragmentViewDestroyedonFragmentDestroyed 方法,两个都会调用RefWatcher的watch方法,前者watch的是View,后者watch的时Fragment
  5. 后面的流程就和Activity 泄漏检测相同了。

2、LeakCanary是如何处理泄漏结果的?

  • 手动触发GC来dump文件,根据添加的对象的引用情况,来判断是否有内存泄漏发生;
  • 如果有内存泄漏发生,会采用DisplayLeakService来进行分析;
  • 分析是找到最短泄漏路径,组装好数据,交给UI层来处理;

常用名词解释

  1. RefWatcher
    即 Watcher Reference,用来监听弱引用的
  2. RefWatcherBuilder 和 AndroidRefWatchBuilder
    用来进行RefWatcher的构造,好多参数都有默认的对应的defaultXXX实现。
  3. IdleHandler
    通过王主线程消息队列中添加IdleHandler,已到达在主线程空闲时来进行内存泄漏分析的目的,这里有一个延时,时间是5s。
  4. IntentService
    LeakCanary中大量的用到了IntentService,如HeapAnalyzerServiceDisplayLeakService,前者分析HeapDump文件并得到分析结果最终保存到文件,后者解析分析到的结果并开启DisplayLeakActivity最终在UI上进行展示。IntentService会在创建后,开启一个HandlerThread,这就可以很好的进行分析文件、处理分析结果这些耗时任务了。

几个问题

  1. Dumping memory, app will freeze. Brrr是如何产生的?

    Dumping memory, app will freeze. Brrr这句话是AndroidHeapDumpershowToast方法里面输出的,里面采用CountDownLatch,让其他线程等待5秒,起到冻结App的作用

  2. LeakCanary桌面上的Leak图标和Block图标是如何产生的?

    看leakcanary-android包下面的AndroidManifest.xml文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <activity
    android:name="com.squareup.leakcanary.internal.DisplayLeakActivity"
    android:enabled="false"
    android:icon="@mipmap/leak_canary_icon"
    android:label="@string/leak_canary_display_activity_label"
    android:process=":leakcanary"
    android:taskAffinity="com.squareup.leakcanary.${applicationId}"
    android:theme="@style/leak_canary_LeakCanary.Base" >
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />

    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>

    DisplayLeakActivity的声明就可以知道了,它定义了显示icon,并且是一个android.intent.action.MAIN,同时还是android.intent.category.LAUNCHER

  3. 分析结果的类型

    分析的结果有三种,在AnalysisResult中定义

    • noLeak,没有泄漏
    • leakDetected,有泄漏,此时也分多种情况:
      • 发现的泄漏是已知的被排除在外的,可以通过exclude关键字来判断,这种泄漏可以忽略不处理;
      • 发现的泄漏是未知的,此时需要对泄漏进行处理,统计泄漏的信息;
    • failure,泄漏检测失败
  4. LeakCanary如何确保调用gc后就会触发弱引用回收的?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void runGc() {
    // Code taken from AOSP FinalizationTest:
    // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
    // java/lang/ref/FinalizationTester.java
    // System.gc() does not garbage collect every time. Runtime.gc() is more likely to perfom a gc.
    Runtime.getRuntime().gc();//调用Runtime.gc()来执行GC操作
    enqueueReferences();//等待100ms中,等待弱引用对象进入引用队列中
    System.runFinalization();//执行对象的finalize()方法
    }

    其实,gc的调用并不是有你是否调用System.gc()或者Runtime.getRuntime().gc()决定的,调用这两个方法知识告诉JVM需要GC,JVM的GC是周期性自动运行的,是有JVM决定运行时机。

  5. 排除一些已知内存泄漏的方法?

    自定义ExcludeRef,其实有一个默认的实现可供参考。

小结

本文通过跟踪源码,了解到了LeakCanary内存泄漏检测的基本原理,并对之前自己存在的几点疑问都做了一些简单的解答。

参考文章

https://cloud.tencent.com/developer/article/1750743