LeakCanary 原始碼分析

梁山boy發表於2018-12-11

1. 前言

LeakCanary 是由 Square 開發的一款記憶體洩露檢測工具。相比與用 IDE dump memory 的繁瑣,它以輕便的日誌被廣大開發者所喜愛。讓我們看看它是如何實現的吧。

ps: Square 以著名框架 Okhttp 被廣大開發者所熟知。

2. 原始碼分析

2.1 設計架構

分析一個框架,我們可以嘗試先分層。好的框架層次清晰,像TCP/IP那樣,一層一層的封裝起來。這裡,我按照主流程大致分了一下。

一圖流,大家可以參考這個圖,來跟原始碼。

java_concurrent

2.2 業務層

按照教程,我們通常會有如下初始化程式碼:

  1. Applicaion 中:mRefWatcher = LeakCanary.install(this);
  2. 基類 Activity/Fragment onDestory() 中: 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() 是否能立即觸發 gc,這點感覺也比較含糊。這是一個 native 方法,依賴於 JVM 的實現,深究起來需要去看 Dalvik 的原始碼,先打一個問號。
  • 框架中 gc 的這段程式碼是從 AOSP 裡拷貝出來的。。。所以說,多看原始碼是個好習慣。

leakcanary-no-op

  • release 版提供一個空實現,可以學習一下。

相關文章