LeakCanary 源码分析

栏目: 编程工具 · 发布时间: 5年前

内容简介:LeakCanary 是由 Square 开发的一款内存泄露检测工具。相比与用 IDE dump memory 的繁琐,它以轻便的日志被广大开发者所喜爱。让我们看看它是如何实现的吧。ps: Square 以著名框架 Okhttp 被广大开发者所熟知。分析一个框架,我们可以尝试先分层。好的框架层次清晰,像TCP/IP那样,一层一层的封装起来。这里,我按照主流程大致分了一下。

LeakCanary 是由 Square 开发的一款内存泄露检测工具。相比与用 IDE dump memory 的繁琐,它以轻便的日志被广大开发者所喜爱。让我们看看它是如何实现的吧。

ps: Square 以著名框架 Okhttp 被广大开发者所熟知。

2. 源码分析

2.1 设计架构

分析一个框架,我们可以尝试先分层。好的框架层次清晰,像TCP/IP那样,一层一层的封装起来。这里,我按照主流程大致分了一下。

一图流,大家可以参考这个图,来跟源码。

LeakCanary 源码分析

2.2 业务层

按照教程,我们通常会有如下初始化代码:

mRefWatcher = LeakCanary.install(this);
mRefWatcher.watch(this);

虽然是用户端的代码,不过作为分析框架的入口,不妨称为业务层。

这一层我们考虑的是检测我们的业务对象 Activity。当然你也可以用来检测 Service。

2.3 Api层

从业务层切入,我们引出了两个类 LeakCanaryRefWatcher ,组成了我们的 api 层。

这一层我们要考虑如何对外提供接口,并隐藏内部实现。通常会使用 Builder单例 、适当的 包私有权限

2.3.1 主线1 install()

public final class LeakCanary {

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

我们先看 install() ,先拿到一个 RefWatcherBuilder ,转而使用 Builder 模式构造一个 RefWatcher 作为返回值。

大概可以知道是框架的一些初始配置。忽略其他,直接看 buildAndInstall()

public final class AndroidRefWatcherBuilder extends RefWatcherBuilder<AndroidRefWatcherBuilder> {
  ...
  private boolean watchActivities = true;
  private boolean watchFragments = true;
  
  public @NonNull RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      ...
      if (watchActivities) { // 1
        ActivityRefWatcher.install(context, refWatcher);
      }
      if (watchFragments) {  // 2
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    return refWatcher;
  }
}

可以看到 1, 2 两处,默认行为是,监控 Activity 和 Fragment。

以 Activity为例:

public final class ActivityRefWatcher {

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

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

使用了 Application.ActivityLifecycleCallbacks ,看来我们基类里的 watch() 是多余的。Fragment 也是类似的,就不分析了,使用了 FragmentManager.FragmentLifecycleCallbacks

PS: 老版本默认只监控 Activity,watchFragments 这个字段是 2018/6 新增的。

2.3.2 主线2 watch()

之前的分析,引出了 RefWatcher.watch() ,它可以检测任意对象是否正常销毁,不单单是 Activity。我们来分析看看:

public final class RefWatcher {

  private final WatchExecutor watchExecutor;

  public void watch(Object watchedReference, String referenceName) {
    ...
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

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

通过这个 watch(),我们可以注意到这几点:

  1. 为了不阻塞我们的 onDestory() ,特意设计成异步调用—— WatchExecutor
  2. 有一个弱引用 KeyedWeakReference ,干嘛用的呢?

我们该怎么设计 WatchExecutor 呢?AsyncTask?线程池?我们接着往下看

2.4 日志产生层

现在我们来到了非常关键的一层,这一层主要是分析是否泄露,产物是 .hprof文件

我们平常用 IDE dump memory 的时候,生成的也是这种格式的文件。

2.4.1 WatchExecutor 异步任务

接之前的分析, WatchExecutor 主要是用于异步任务,同时提供了失败重试的机制。

public final class AndroidWatchExecutor implements WatchExecutor {
  private final Handler mainHandler;
  private final Handler backgroundHandler;

  public AndroidWatchExecutor(long initialDelayMillis) {
    mainHandler = new Handler(Looper.getMainLooper());
    HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
    handlerThread.start();
    backgroundHandler = new Handler(handlerThread.getLooper());
    ...
  }

  @Override public void execute(@NonNull Retryable retryable) {
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      waitForIdle(retryable, 0);
    } else {
      postWaitForIdle(retryable, 0);
    }
  }
  ...
}

看来是使用了 HandlerThread 。没啥说的,要注意一下子线程 Handler 的使用方式。之后便会回调 ensureGone() ,注意此时执行环境已经切到子线程了。

2.4.2 ReferenceQueue 检测泄露

分析下一步之前,我们先介绍一下 ReferenceQueue

  1. 引用队列 ReferenceQueue 作为参数传入 WeakReference.
  2. WeakReference 中的 value 变得不可达,被 JVM 回收之前,WeakReference 会被加到该队列中,等待回收。

说白了,ReferenceQueue 提供了一种通知机制,以便在 GC 发生前,我们能做一些处理。

详见 Reference 、ReferenceQueue 详解

好了,让我们回到 RefWatcher。

final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;		// 由于真正的 value 正等待回收,我们追加一个 key 来识别目标。
  public final String name;

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

public final class RefWatcher {
  private final Set<String> retainedKeys;  // 保存未回收的引用的 key。 watch()时 add, 在 queue 中找到则 remove。
  private final ReferenceQueue<Object> queue; // 收集所有变得不可达的对象。

  public void watch(Object watchedReference, String referenceName) {
    ...
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);  // 1
    final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }

  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    removeWeaklyReachableReferences();	// 2
    ...
    if (gone(reference)) {  // 3
      return DONE;
    }
    gcTrigger.runGc();	// 4
    removeWeaklyReachableReferences();	// 5
    if (!gone(reference)) {	// 6
      // 发现泄漏
      ...
    }
    return DONE;
  }
  
  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }

  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);
    }
  }
}

我们有这样的策略:用 retainedKeys 保存未回收的引用的 key。

  1. 主线程 onDestroy() -> watch() -> retainedKeys.add(ref.key)。WatchExecutor 启动,主线程 Activity 销毁。
  2. WatchExecutor.execute() -> ensureGone() -> removeWeaklyReachableReferences() -> 遍历 ReferenceQueue,从 retainedKeys.remove(ref.key)
  3. 判断 gone(ref), 如果 Activity 已经不可达,那么直接返回,否则可能有内存泄漏。

4-6. 引用还在,然而这里没有立即判定为泄漏,而是很谨慎的手动触发 gc,再次校验。

2.4.3 GcTrigger 手动触发 Gc

这里注意一点 Android 下边的 jdk 和 oracle 公司的 jdk 在一些方法的实现上有区别。比如这个 System.gc() 就被改了,不再保证必定触发 gc。作者使用 Runtime.getRuntime().gc() 作为代替。

了解更多: System.gc() 源码解读

2.4.4 HeapDumper 生成堆快照 .hprof

public final class RefWatcher {
  private final HeapDumper heapDumper;
  private final HeapDump.Listener heapdumpListener;

  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    ...
    gcTrigger.runGc();	
    removeWeaklyReachableReferences();	
    if (!gone(reference)) {	
      // 发现泄漏
      File heapDumpFile = heapDumper.dumpHeap();

      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile)
          ...
          .build();

      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }
}

我们跟进 heapDumper.dumpHeap(),略去一些 UI 相关代码:

public final class AndroidHeapDumper implements HeapDumper {

  @Override @Nullable 
  public File dumpHeap() {
    File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
    ...
    try {
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
      ...
      return heapDumpFile;
    } catch (Exception e) { ... }
  }
}

最后用了 Android 原生的 api —— Debug.dumpHprofData() ,生成了堆快照。

2.5 日志分析层 && 日志展示层

生成 .hprof 之后,之后由 heapdumpListener.analyze(heapDump) 把数据转到下一层。其实这两层没啥好分析的, .hprof 已经是标准的堆快照格式,平时用 AS 分析内存生成的也是这个格式。

所以,LeakCanary 在这一层只是帮我们读取了堆中的引用链。然后,日志展示层也没啥说的,就一个 ListView。

3. 总结

最后,我们可以看到一个优秀的框架需要那些东西:

分层

  • 分层的意义在于逻辑清晰,每一层的任务都很明确,尽量避免跨层的依赖,这符合单一职责的设计原则。
  • 对于使用者来说,只用关心 api层 有哪些接口以及 业务层 怎么使用;而对于维护者来说,很多时候只需要关心核心逻辑 日志产生层 ,UI层不怎么改动丑一点也没关系。方便使用也方便维护。

ReferenceQueue 的使用

  • 学到了如何检测内存回收情况,并且做一些处理。以前只会傻傻的 new WeakReference()

手动触发 gc

Runtime.getRuntime().gc()
AOSP

leakcanary-no-op

  • release 版提供一个空实现,可以学习一下。

以上所述就是小编给大家介绍的《LeakCanary 源码分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

白帽子讲浏览器安全

白帽子讲浏览器安全

钱文祥 / 电子工业出版社 / 2016-3 / 79.00元

浏览器是重要的互联网入口,一旦受到漏洞攻击,将直接影响到用户的信息安全。作为攻击者有哪些攻击思路,作为用户有哪些应对手段?在《白帽子讲浏览器安全》中我们将给出解答,带你了解浏览器安全的方方面面。《白帽子讲浏览器安全》兼顾攻击者、研究者和使用者三个场景,对大部分攻击都提供了分析思路和防御方案。《白帽子讲浏览器安全》从攻击者常用技巧的“表象”深入介绍浏览器的具体实现方式,让你在知其然的情况下也知其所以......一起来看看 《白帽子讲浏览器安全》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码