剖析LeakCanary—— 中篇

为善不见其益,如草里冬瓜,自能暗长;
为恶不见其损,如庭前春雪,势必潜消。

官网的一句话:千里之堤,溃于蚁穴

“A small leak will sink a great ship.” - Benjamin Franklin


LeakCanary 这个库,作为Android开发者应该都不会陌生。它的主要作用就是帮助我们找出内存泄漏的地方,帮助我们开发者减少OOM的发生。

1. LeakCanary简述

我们正式进入LeakCanary的探索中。详情请查看LeakCanary的文档

通过查看LeakCanary的文档,发现2.x的版本和1.x的版本差异还是比较大的。具体内容:升级到LeakCanary2

1.1 LeakCanary2的特点

  1. 新的堆分析工具,速度快且内存占用率低; Shark
  2. 更新APIs来简化配置,开放新的堆分析器的权限;
  3. 100% Kotlin 重写;
  4. 一次分析过程可以探测多种泄漏类型,并按照类型分组;

心动不如行动,抓紧升级吧


2. LeakCanary 原理

由于项目中目前还在使用的是LeakCanary 1.x版本, 而 LeakCanary2的升级变化又比较大。这里就简单介绍一下1.x的版本,后续补上2.x版本的相关信息。

2.1 核心类 RefWatcher

1.1 RefWatcher所涉及的内容

public final class RefWatcher {
  // 用来观察泄漏情况的执行器,内部使用的是IdleHandler
  private final WatchExecutor watchExecutor;
  private final DebuggerControl debuggerControl;
  // GC开关 -- 用来手动触发GC操作的
  private final GcTrigger gcTrigger;
  // 用于导出堆信息文件
  private final HeapDumper heapDumper;
  // 用于监听分析堆对象(HeapDump)的过程
  private final HeapDump.Listener heapdumpListener;
  // 堆对象的建造者
  private final HeapDump.Builder heapDumpBuilder;
  // 用于存储回收的弱引用映射的Key
  private final Set<String> retainedKeys;
  // 引用队列,
  private final ReferenceQueue<Object> queue;
 }

这么多参数,而且都可以有默认实现—— 建造者模式

自然就产生 RefWatcherBuilder

又因为我们主要在Android平台上使用,所以Squareup 就又为我们特有的AndroidRefWatcherBuilder

讲到这里,我们就应该明白我们的重点关注对象应该是:

  1. RefWatcher :判断内存泄漏的相关逻辑都在这里
  2. AndroidRefWatcherBuilder:针对于我们Android平台,肯定有其独特之处。

当然,对Builder模式感兴趣的朋友可以关注一下RefWatcherBuilder的实现逻辑。

1.2 RefWatcher的功能实现

LeakCanary虽然总体思路跟我们差不多,但是具体实现还是很值得我们学习的。

1.2.1 监测功能
public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    // 通过UUID,给每个监测的引用对象绑定一个key
    String key = UUID.randomUUID().toString();
    // 添加集合,后面通过集合来判断对应的引用是否被回收
    retainedKeys.add(key);
    // 注意这里使用了引用队列queu,果然不出你之所料
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
	// 判断引用是否能回收等相关操作(async 异步处理)
    ensureGoneAsync(watchStartNanoTime, reference);
  }
1.2.2 判断及处理功能

如果发现其内存泄漏,我们可以自己触发GC功能;若还是泄漏,我们应该分析并向开发者展示其泄漏相关信息。LeakCanary 也是这么做的。(不服不行)

  1. ensureGoneAsync()
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
	// 眼不眼熟,莫非是线程池?先保留悬念,如果是Java程序,肯定应该是(守护)线程池
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
      	// 真正的逻辑在这里
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }
  1. ensureGone()
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
	// 1. 遍历引用队列
    removeWeaklyReachableReferences();
    // 2. 判断该引用是否被回收,若回收则直接返回
    if (gone(reference)) {
      return DONE;
    }
    // 该引用没有被回收,手动触发一次GC
    gcTrigger.runGc();
    // 再次 遍历引用队列
    removeWeaklyReachableReferences();
    // 如果还是没有被回收,则需要进行堆内存分析等相关操作
    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 heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();
	  // 执行分析操作
      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }
  1. 相关方法
 // 判断该引用是否存在泄漏情况
 private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }
 // 如果没有内存泄漏,则队列中的元素不为null,则遍历集合删除其对应的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);
    }
  }

文章到这里,LeakCanary实现原理的整体逻辑基本呈现出来啦。不知道读到这里的朋友们有没有疑问:

为什么要额外使用HastSet 呢? 不使用行不行呢?

首先我认为不借助于HashSet肯定是可以的,我们可以这样实现:

private boolean removeReferences(KeyedWeakReference reference) {
    KeyedWeakReference ref;
    boolean result ;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      if(ref == reference){
          result =  true;
      }
    }
    return result;
}

可能使用HashSet的目的是为了反转逻辑,更有利于理解吧。(PS:个人之愚见)

判断内存泄漏
不使用HashSet:根据 ReferenceQueue 不存在其内容
使用HashSet : 根据 HashSet 存在其内容

2.2 HeadDump 堆内存分析对象

综合考虑,还是建议把该类提前进行简要分析一下

public final class HeapDump implements Serializable {
//1.用户监听堆对象分析过程的借口
public interface Listener {
    Listener NONE = new Listener() {
      @Override public void analyze(HeapDump heapDump) {
      }
    };

    void analyze(HeapDump heapDump);
  }
  /** The heap dump file, which you might want to upload somewhere. */
  // 2. 正如注释所讲,有了此文件,自己也可以分析啊。(家有存粮,心中不慌)
  public final File heapDumpFile;
  // 3. 这两个属性,其实就是用于辨别泄漏对象用的。
  public final String referenceKey;
  public final String referenceName;
  
  // 4. 划重点,用于过滤那些不用被分析的应用的。(主次分明)
  public final ExcludedRefs excludedRefs;
  
  ...
}

总结一下:

  1. 用于监听分析的接口;
  2. 堆内存分析的文件;
  3. 识别相应泄漏引用的标示;
  4. 过滤不必要分析的东西;

不得不感慨一下,就算知道怎么写,也思虑不周啊。


2.3 LeakCanary的总体脉络

当我们真正掌握了引用队列等相关知识后,我们就明白整个Java体系都可以利用引用队列的特点来监测内存泄漏情况。通过阅读LeakCanary的源码,也印证了我们的猜想,可以看出来LeakCanary并不是只针对于Android平台开发的。

这篇文章并不涉及Android的相关东西,这里结合LeakCanary源码对其进行一次概括性的总结。

2.3.1 人物

WatchExecutor :后台监测内存泄漏;
RefWatcher :收集,处理并分析内存泄漏;
HeapDumper : 产生用于内存分析的文件
HeapDump : 分析材料和结果
ExcludedRefs :过滤不必分析的内容

2.3.2 过程
  1. 建造一个WatchExecutor(Java中大概率是线程池),在合适的时机监听需要观察的应用。
  2. RefWatcher 给每个引用通过UUID映射,并作出相关的收集,处理工作。如果处理不掉(内存泄漏),则利用HeapDumper生成文件File,并由文件产生HeapDump对象(若有不需要分析的,就借助于ExcludedRef),通过其Listener进行分析并监听分析结果。

至于用于分析堆内存信息,是借助于haha库,有兴趣的朋友可以点进去详细了解。PS : 该库已废弃.

下面就让我们来见识一下:
LeakCanary在Android系统上所作的工作 剖析LeakCanary—— 下篇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章