LeakCanary 原理分析

本文主要内容

  • 1、Reference 简介
  • 2、LeakCanary 使用
  • 3、LeakCanary 源码分析

LeakCanary ,一种常见的内存泄漏分析工具,它能分析出内存泄漏点并以通知形式告诉使用者,使用也比较简单,但功能强大。

笔者第一次见到 LeakCanary 时,并不清楚它的原理,了解到它只是一个开源工程并不是官方工具之后,觉得作者太牛逼了,内存泄漏都可以检测。今天我们一起来看 LeakCanary 的源码,揭开它的神秘面纱。

1、Reference 简介

java中存在四种引用,重新温习一遍四种引用的用法及作用:

  • 强引用:最普遍的引用,声明一个变量就是强引用,比如 obj ,当它被置为null时,该对象可能会被 JVM 回收,因为还要看是否有其它强引用指向它。

    Object obj = new Object();
    
  • SoftReference,软引用:当内存不够用的时候,才会回收软引用

  • WeakReference,弱引用: new出来的对象没有强引用连接时,下一次GC时,就会回收该对象。

  • PhantomReference,虚引用 : 与要与ReferenceQueue配合使用,它的get()方法永远返回null

SoftReference、WeakReference、PhantomReference等类都位于 java.lang.ref 包中,它们都有一个共同的父类,Reference 。

java.lang.ref包下主要都是reference相关的类,主要包括:

  • FinalReference: 代表强引用,使没法直接使用。
  • Finalizer:FinalReference的子类,主要处理finalize相关的工作
  • PhantomReference: 虚引用
  • Reference: 引用基类,abstract的
  • ReferenceQueue: 引用轨迹队列
  • SoftReference:软引用
  • WeakedReference: 弱引用

下面,阐述关于Reference 相关的一个结论:

Reference引用的对象被回收时,Reference 对象将被添加到 ReferenceQueue中,前提是构造 Reference 时,参数中有 ReferenceQueue。

如果要监听某个对象是否被回收,有什么办法呢?

Object obj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakedReference  r = new WeakedReference(ojb, queue);

根据上面的结论,如果 obj 对象被回收了,那么 queue 将添加 r,那么我们可以查找队列,如果有r,则证明 obj 对象被回收了,监控完成。

查看下 Reference 的源码,它里边的关键代码如下:

// 静态变量,pending 
private static Reference pending = null;
// 线程,不停地回收 pending 
private static class ReferenceHandler extends Thread {

    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        for (;;) {

            Reference r;
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    Reference rn = r.next;
                    pending = (rn == r) ? null : rn;
                    r.next = r;
                } else {
                    try {
                        lock.wait();
                    } catch (InterruptedException x) { }
                    continue;
                }
            }

            // Fast path for cleaners
            if (r instanceof Cleaner) {
                ((Cleaner)r).clean();
                continue;
            }
            // 将 r 添加到 ReferenceQueue 中
            ReferenceQueue q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
        }
    }
}

看到这里,可能有同学会提问,pending 在哪被赋值的?怎么知道它就是要被回收的 Reference 呢?确实,这个问题我也没有从源码中查到,从网上找的资料来看,都说 pending 由 JVM 维护。Reference 有个成员变量next,它可以很轻松地变成一个链表,而pending 正是一个链表的头节点,如果某个 Reference 将被回收,它将被 添加到pending 的链表当中,如果它马上要被回收了,ReferenceHandler 线程将它添加到 ReferenceQueue 中。

通过 Reference 的学习,感觉 LeakCanary 最大的难题解决了,如何判定一个对象是否被回收了。

2、LeakCanary 使用

LeakCanary 使用非常简单,首先在你项目app下的build.gradle中配置:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
  releaseImplementation   'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
  // 可选,如果你使用支持库的fragments的话
  debugImplementation   'com.squareup.leakcanary:leakcanary-support-fragment:1.6.2'
}

然后在你的Application中配置:

public class WanAndroidApp extends Application {

@Override public void onCreate() {
  super.onCreate();
  if (LeakCanary.isInAnalyzerProcess(this)) {
    // 1
    return;
  }
  // 2
  refWatcher = LeakCanary.install(this);
}

使用就是这么得简单。

怎么判断一个对象已死呢?内存可达性算法,即从一个根对象出发,如果无法寻到一条路径指向该对象,则对象已死。

假设是我们自己来设计 LeakCanary ,我们会怎么去设计呢?目前已经可以监控某个对象是否已经被回收了。我们也不可能去监听所有的对象吧,这样不现实,肯定是去找特定对象来监控,在Android中,常见的内存泄漏都会导致 activity 无法被回收,activity 就是最特定的对象,所以,可以监听 activity。

LeakCanary 正是这样设计的,它目前可以监听 activity 和 fragment。

3、LeakCanary 源码分析

先看看 install 方法:

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

install 方法返回 RefWatcher 对象,这是一个链式调用,我们一步步地来看。

public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
return new AndroidRefWatcherBuilder(context);
}

refWatcher 方法返回 AndroidRefWatcherBuilder 对象,从名字可知它是一个构造器,builder,它的构造函数也很简单,保存context

AndroidRefWatcherBuilder(@NonNull Context context) {
this.context = context.getApplicationContext();
}

继续回到 install 方法,查看 listenerServiceClass 方法:

public @NonNull AndroidRefWatcherBuilder listenerServiceClass(
  @NonNull Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
enableDisplayLeakActivity = DisplayLeakService.class.isAssignableFrom(listenerServiceClass);
return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
}

public final T heapDumpListener(HeapDump.Listener heapDumpListener) {
this.heapDumpListener = heapDumpListener;
return self();
}

AndroidRefWatcherBuilder 继承自 RefWatcherBuilder ,上面的代码就是为 AndroidRefWatcherBuilder 赋值一个成员变量,heapDumpListener 。继续回到 install 方法

excludedRefs(AndroidExcludedRefs.createAppDefaults().build())

这句话的意思是,排除各种已经的不是内存泄漏的情况。其实为什么要在第一步中指出,这是一个 Builder呢,我们常见的 builder 代码里,有各种各样的赋值,但最关键的代码往往只是它的 build 方法,我们别被这种链式调用弄晕了,这些不重要,别死抠细节不放,大概明白意思就行,接下来我们看最重要的方法:

public @NonNull RefWatcher buildAndInstall() {
if (LeakCanaryInternals.installedRefWatcher != null) {
  throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
}
//调用build方法,构建 RefWatcher 
RefWatcher refWatcher = build();
if (refWatcher != DISABLED) {
  if (enableDisplayLeakActivity) {
    LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
  }
  if (watchActivities) {
    //监听activity
    ActivityRefWatcher.install(context, refWatcher);
  }
    //监听 fragment
  if (watchFragments) {
    FragmentRefWatcher.Helper.install(context, refWatcher);
  }
}
LeakCanaryInternals.installedRefWatcher = refWatcher;
return refWatcher;
}

build 方法返回 RefWatcher ,比较简单,其实就是将之前链式调用赋值的各个对象,赋值给 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);
}

最最关键的还得是 install 方法,本文中我们只分析 activity 的逻辑,fragment 暂不分析。

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

install 方法中只有三行,第一步,获取 Application 对象,第二步,生成一个 ActivityRefWatcher 对象,第三步,看方法名,貌似是注册了一个监听,一个activity生命周期的监听。看看 lifecycleCallbacks 这个回调对象:

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
  new ActivityLifecycleCallbacksAdapter() {
    @Override public void onActivityDestroyed(Activity activity) {
      refWatcher.watch(activity);
    }
  };

关键代码终于出现,当activity 调用 onDestroyed 方法时,会调用lifecycleCallbacks 中的方法,从而可以拿到 activity 的引用。结合之前 Reference 的分析,要监听对象是否被回收,首先得拿到它的引用,现在 activity 的引用拿到了。继续往下分析。

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

ensureGoneAsync(watchStartNanoTime, reference);
}

通过拿到的 activity 引用,构造 KeyedWeakReference 对象,其实它继承自 WeakReference ,它是一个弱引用。构建弱引用的同时,在构造函数中添加 ReferenceQueue,当 activity 被回收时,KeyedWeakReference 对象会被添加到ReferenceQueue当中。如果出现内存泄漏,则 ReferenceQueue 找不到对应的 KeyedWeakReference 对象,那么就可以判断发生内存泄漏了。

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

ensureGoneAsync方法中,通过 watchExecutor 执行一个 run 方法,watchExecutor 只会在主线程中执行它,继续查看 ensureGone方法

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

removeWeaklyReachableReferences();

if (debuggerControl.isDebuggerAttached()) {
  // The debugger can create false leaks.
  return RETRY;
}
//gone方法即是查看到 activity 已经被回收,则返回 DONE,表示内存无漏泄
if (gone(reference)) {
  return DONE;
}
//调用 gc
gcTrigger.runGc();
removeWeaklyReachableReferences();
//调用gc之后,再次检查 ,如果 activity 还没被回收,则是有内存泄漏了
if (!gone(reference)) {
  long startDumpHeap = System.nanoTime();
  long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

  File heapDumpFile = heapDumper.dumpHeap();
  if (heapDumpFile == RETRY_LATER) {
    // Could not dump the heap.
    return RETRY;
  }
  long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

  HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
      .referenceName(reference.name)
      .watchDurationMs(watchDurationMs)
      .gcDurationMs(gcDurationMs)
      .heapDumpDurationMs(heapDumpDurationMs)
      .build();
  // 分析 heap 的prof文件,找出内存泄漏点
  heapdumpListener.analyze(heapDump);
}
return DONE;
}

如果 activity 已经被回收,那么 ReferenceQueue 中将添加指向它的 Reference,这是一条大原则,一定要记住。在 watch 方法时,为每个 activity构造对应的Reference时,还添加了一个key,并把key添加到一个set当中。

private void removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
  retainedKeys.remove(ref.key);
}
}

遍历 ReferenceQueue 中得到的Reference,拿到 Reference,同时删除 set 中对应的key。所以,set中不包含某个 key,则说明对应的 activity已经被回收,反之则是没有回收。

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

gone方法正好验证这点,如果gone返回为true,那么整个过程也结束了。如果gone返回为false,则表明可能有内存泄漏,所以执行一次gc之后 ,再次调用gone方法,查看是否有无泄漏。如果还有泄漏,则分析生成的prof文件,找出关键的泄漏路径。关于如何分析 prof 文件,此处不展开了,其实 LeakCanary 也是借用其它的开源库来分析的。

最后总结下整个过程:

在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与ReferenceQueque关联。这时再从ReferenceQueque中查看是否有没有该对象,如果没有,执行gc,再次查看,还是没有的话则判断发生内存泄露了。最后用HAHA这个开源库去分析dump之后的heap内存。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容