剖析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—— 下篇

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