LeakCanary詳解與原始碼分析

仰簡發表於2018-12-14

一、前言

1.簡介

A small leak will sink a great ship —— Benjamin Franklin

千里之堤,毀於蟻穴。這篇文章要分析的就是squareLeakCanary。LeakCanry主要是用於檢測 Activity 以及 Fragment 中是否存在記憶體洩漏,並且自動彈出通知告知使用者發生了記憶體洩漏,且最終以 UI 的形式向我們展示記憶體洩漏物件的引用鏈,以便我們能精確的定位到記憶體洩漏的程式碼。

image.png

2.使用

2.1新增依賴

dependencies {
  // debug 版本依賴
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
  // release 版本依賴  
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
  // 如果使用了 support fragment,請同時依賴
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.2'
}
複製程式碼

2.2 初始化並開始檢測記憶體洩漏

一般情況下,在你的 Application#onCreate() 方法裡面進行初始化

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // 不檢測 LeakCanary 的分析程式
      return;
    }
    // 初始化並監聽記憶體洩漏
    LeakCanary.install(this);
    .....
  }
}
複製程式碼

二、框架概述

1.框架

優秀的框架之所以優秀,並不只是它向我們提供了強大的功能和解決了我們的問題,相對更重要的還在於它對模組的清晰劃分以及模組之間層疊有秩的層次。通過對各模組的依賴關係的梳理以及對原始碼的理解,梳理出如下的框架圖。

leakcanary框架圖

**leakcanary-sample:**就是我們的應用程式。

leakcanary-android & leakcanry-android-no-op: LeakCanary 一般建議工作在 Debug 模式下,這時我們依賴 leakcanary-android 就可以了。當有檢測到記憶體洩漏時其會先凍結程式並 dump 出記憶體快照並進行分析。而在 release 模式下就依賴 leakcanry-android-no-op,其不會產生後續的分析行為,看架圖的依賴關係就知道了。

leakcanary-analyzer: 主要是通過 haha 進行記憶體洩漏的分析。

leakcanary-watcher: 對物件的引用進行監測,當有記憶體洩漏時先進行 heap dump,然後再告知 leakcanary-analyzer 進行接下來的分析工作。

haha: 這是個 square 的另一個開源庫(haha's github),專門用於自動分析 Android heap dump。類似的庫還有vshor/matAndroMAT 以及鼎鼎大名的 Eclipse Memory Analyzer

2.主體類圖

根據框架圖的層次關係,以及對原始碼的理解,梳理出如下主體類圖。

LeakCanary主體類圖

(1) LeakCanary 以及左邊的方框中的類都是屬於 leakcanary-android 層的,這裡邊除了 LeakCanary 這個對外提供的操作介面,其他基本上是 RefWatcher 中的各個子元件的具體實現類。

(2) RefWatcher 及其框內的東西都是屬於 leakcanary-watcher 層的。這裡的 RefWatcher 是通過 AndroidRefWatcherBuilder 來構造的。通過繼承 RefWatcherBuilder 以及實現各個子元件的介面,我們也可以實現自己的 watcher 層。各個子元件的作用在後面的程式碼分析過程還會再詳細說明。

(3) HeapAnalyzer 屬於 leakcanary-anlyzer層,主要就是分析 heap hprof 檔案並最終通知給應用層。

3.工作原理

看了框架圖以及主體類圖後對 LeakCanary 這個框架應該有一個比較全域性的認知了,在這個基礎上,我們再簡要過一下它的基本工作原理。先來看看它的原理圖。

工作原理

(1) App,也就是我們的應用通過 LeakCanary 這個對外提供的介面來初始化好框架,其主要是初始化好 RefWatcher。

(2) 通過在一定的時機,框架內實現的是監聽 Activity 與 Fragment 的生命週期,向 RefWatcher 新增監測的引用。RefWatcher 在主執行緒 idle 後或者進行一次強制 gc 後再判斷該引用是否已經被回收來判定其是否有記憶體洩漏。

(3) 當 RefWatcher 檢測到有記憶體洩漏後,便會通過其元件 HeapDumper dump 出記憶體堆的 hprof 檔案,並交由 HeapAnalyzer 進行分析。

(4) HeapAnalyzer 分析出結果後便會通知到 App。

LeakCanary 內部只幫我們監測了 Activity 以及 Fragment 存在的記憶體洩漏問題,理論上來說,我們在對 App 的業務理解基礎也能找出可以監測的時機,從而監測更多的記憶體洩漏的問題。

三、原始碼分析

1.demo

LeakCanary 不同於其他的庫的使用,其非常簡單,這在第一部分也已經介紹了,其主要就是一句程式碼的事兒。

LeakCanary.install(this);
複製程式碼

程式碼分析就從這裡開始了。

2.初始化 install()

先來看一看 LeakCanary 的時序圖,分析程式碼時,也是沿著時序圖一步一步來分析。

LeakCanary初始化時序圖

2.1 LeakCanary#install()

/**
   * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
   * references (on ICS+).
   */
  public static @NonNull RefWatcher install(@NonNull Application application) {
    return 
        // 建立 AndroidRefWatcherBuilder
        refWatcher(application)
        // 設定用於監聽記憶體洩漏的分析結果的 Service
        .listenerServiceClass(DisplayLeakService.class)
        // 忽略檢測的引用
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        // 構建並且初始化 RefWatcher
        .buildAndInstall();
  }
複製程式碼

方法中先通過 refWatcher() 建立了 AndroidRefWatcherBuilder,這是一個 builder 設計模式,接下來是一系列的鏈式調設定引數,最後再通過 buildxxx() 來構建出最後具體的物件。refWatcher() 的程式碼很簡單,就是 new 一個 AndroidRefWatcherBuilder 物件,如下。 LeakCanary#refWatcher()

  public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
    return new AndroidRefWatcherBuilder(context);
  }
複製程式碼

2.2 AndroidRefWatcherBuilder#listenerServiceClass()。

  /**
   * Sets a custom {@link AbstractAnalysisResultService} to listen to analysis results. This
   * overrides any call to {@link #heapDumpListener(HeapDump.Listener)}.
   */
  public @NonNull AndroidRefWatcherBuilder listenerServiceClass(
      @NonNull Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
  }
複製程式碼

方法的主要作用是設定一個用於監聽記憶體洩漏的分析結果的 Service,這裡傳入的是 DisplayLeakService。

/**
 * Logs leak analysis results, and then shows a notification which will start {@link
 * DisplayLeakActivity}.
 * <p>
 * You can extend this class and override {@link #afterDefaultHandling(HeapDump, AnalysisResult,
 * String)} to add custom behavior, e.g. uploading the heap dump.
 */
public class DisplayLeakService extends AbstractAnalysisResultService {......}
複製程式碼

從它的定義中的註釋可知,這是當有分析結果後,其便會發出一個 notification,並且通過這個 notification 來啟動 DisplayLeakActivity,就是我們上面截圖看到的那個 Activity。關於 DisplayLeakService ,它的類圖以及繼承關係如下。

Service.jpg

可以看出這是一個 IntentServcie,即完成工作後會主動退出 Service 而不會長期佔用記憶體。在這個類圖中,DisplayLeakService 主要需要實現鉤子函式 onHeapAnalyzed(),在這裡完成其主要的工作。

然後再來看 ServiceHeapDumpListener 的定義。

public final class ServiceHeapDumpListener implements HeapDump.Listener {
  .....
  @Override public void analyze(@NonNull HeapDump heapDump) {
    ......
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }
}
複製程式碼

這是 HeapDump.Listener 的具體實現類,主要工作在 hook 方法 analyze() 裡面,就是當發生記憶體洩漏,並且 dump 完了 heap hprof 後便會回撥到該方法,然後通過 HeapAnalyzerService.runAnalysis() 呼叫來啟動 HeapAnalyzerService 來進行 heap hprof 的分析。具體的 heap hprof 分析後面還會細講。

最後是 heapDumpListener() 的呼叫,該方法的呼叫就是將剛才 new 出來的 ServiceHeapDumpListener 儲存在屬性 heapDumpListener 裡。

**小結:**這裡小結一下,通過 listenerServiceClass() 呼叫確定了兩個點: (1) 由 ServiceHeapDumpListener 監聽記憶體洩漏的通知,並啟動 HeapAnalyzerService 來進行具體的分析工作。

(2) 由 DisplayLeakService 監聽 heap hprof 分析完成後的工作,然後通過在狀態列發出 notification 來通知使用者。

2.3 RefWatcherBuilder#excludedRefs()

  public final T excludedRefs(ExcludedRefs excludedRefs) {
    heapDumpBuilder.excludedRefs(excludedRefs);
    return self();
  }
複製程式碼

主要是設定那些用於忽略的已知的記憶體洩漏,這個其實並不是主路徑,那就先暫時不深入展開了。 ###2.4 AndroidRefWatcherBuilder#buildAndInstall()

/**
   * Creates a {@link RefWatcher} instance and makes it available through {@link
   * LeakCanary#installedRefWatcher()}.
   *
   * Also starts watching activity references if {@link #watchActivities(boolean)} was set to true.
   *
   * @throws UnsupportedOperationException if called more than once per Android process.
   */
  public @NonNull RefWatcher buildAndInstall() {
   ......
    // 建立最終返回的 RefWatcher
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      // 設定 DisplayLeakActivity 為 enable 狀態
      LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
      // 如果需要監測 activity 則啟動 activity 的監測
      if (watchActivities) {
        ActivityRefWatcher.install(context, refWatcher);
      }
      // 如果需要監測 activity 則啟動 activity 的監測
      if (watchFragments) {
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
  }
複製程式碼

方法主要就是最後建立出 RefWatcher 返回給呼叫者,而過程就如方法名先 build 再 install,先來分析 build。

2.4.1 RefWatcherBuilder#build()

public final RefWatcher build() {
    // 如果禁用了返回 RefWatcher.DISABLED
    if (isDisabled()) {
      return RefWatcher.DISABLED;
    }
    //  初始化待忽略的已知記憶體洩漏集
    if (heapDumpBuilder.excludedRefs == null) {
      heapDumpBuilder.excludedRefs(defaultExcludedRefs());
    }
    // 初始化 heap dump 監聽器
    HeapDump.Listener heapDumpListener = this.heapDumpListener;
    if (heapDumpListener == null) {
      heapDumpListener = defaultHeapDumpListener();
    }
    // 初始化 DebuggerController
    DebuggerControl debuggerControl = this.debuggerControl;
    if (debuggerControl == null) {
      debuggerControl = defaultDebuggerControl();
    }
    // 初始化 HeapDumper
    HeapDumper heapDumper = this.heapDumper;
    if (heapDumper == null) {
      heapDumper = defaultHeapDumper();
    }
    // 初始化 WatchExecutor
    WatchExecutor watchExecutor = this.watchExecutor;
    if (watchExecutor == null) {
      watchExecutor = defaultWatchExecutor();
    }
    // 初始化 GcTrigger
    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);
  }
複製程式碼

build() 的工作主要就是初始化其內部的各個元件。讓我們先對這些元件有一個粗略的認知。

(1) HeapDump.Listener,這裡是 ServiceHeapDumpListener。

(2) DebuggerControl,是否 app 被 attach 到 debug 模式下,這種情況不監測。這裡預設就是 AndroidDebuggerControl。

(3) HeapDumper,見名知義,dump 出記憶體堆 heap hprof 並儲存到檔案中。這裡預設是 AndroidHeapDumper。

(4) WatchExecutor,監測呼叫度器,就是在一定延遲後執行一次監測操作。這裡是AndroidWatchExecutor,其主要是在等在主執行緒 Idle (空閒) 後,向後臺執行緒發起檢視記憶體是否洩漏。

(5) GcTrigger,這個也是見名知義,就是 gc 觸發器。

對 RefWatcherBuilder 內部的各個元件有一個認知後,再來分析 install()。

2.4.2 ActivityRefWatcher#install()

  public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
// 註冊 Activity 的生命周的回撥。
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }
複製程式碼

主要的手段是監聽 Activity 宣告週期的回撥,來看一看 lifecycleCallbacks 的實現。

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

其只實現了 onActivityDestroyed()。也就是當 Activity 發生 onDestroy() 後開始監測這個 Activity 是否有記憶體洩漏。進一步來看 refWatcher#watch() 的實現。

public void watch(Object watchedReference, String referenceName) {
    //如果是禁用狀態就返回了
    if (this == DISABLED) {
      return;
    }
    ......
    final long watchStartNanoTime = System.nanoTime();
    // 給引用指定的 key,並將其加入到 retainedKeys 中,將來作為弱引用物件是否已經被回收的依據。
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    // 建立一個弱引用,並指定物件為 activity,同時還指定了 queue。只要WeakReferences指向的物件變得弱可及,它就被加入佇列。這是在最終確定或垃圾收集實際發生之前。
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
    // 觸發一個非同步的排程。
    ensureGoneAsync(watchStartNanoTime, reference);
  }
複製程式碼

主要就是建立一個弱引用併發出一個非同步排程。而這個非同步排程是由 AndroidWatchExecutor 來控制的,也就說其會等待主執行緒 idle 後執行這個排程。那這個排程作了什麼呢?

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

排程裡就一句程式碼,即呼叫了 ensureGone()。

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    // 先把ReferenceQueue中的弱引用從 retainedKeys 中移除。
    removeWeaklyReachableReferences();
    
    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    // 如果當前引用不在 retainedKeys 中,說明已經移除了,也說明弱引用的物件可回收
    if (gone(reference)) {
      return DONE;
    }
    // 如果沒有,則強制觸發一次 gc
    gcTrigger.runGc();
    // 再嘗試移除 retainedKeys
    removeWeaklyReachableReferences();
   // 如果還在說明有記憶體洩漏了。
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      // 發起 heap dump
      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 進行分析
      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }
複製程式碼

**小結:**主要的處理邏輯都在程式碼的註釋裡。這裡再總結一下監測記憶體洩漏的原理:

(1) 監測Activity 的生命週期的 onDestroy() 的呼叫。

(2) 當某個 Activity 的 onDestroy() 呼叫後,便對這個 activity 建立一個帶 ReferenceQueue 的弱引用,並且給這個弱引用建立了一個 key 儲存在 retainedKeys 中。

(3) 在 Android 的 Framework 中,當一個 Activity 被 destroy 後一般會產生一次 gc,並且還會產生一個 idle。

(4) 如果這個 activity 可以被回收,那麼弱引用就會被新增到 ReferenceQueue 中。

(5) 等待主執行緒進入 idle後,通過一次遍歷,在 ReferenceQueue 中的弱引用所對應的 key 將從 retainedKeys 中移除,說明其沒有記憶體洩漏。

(6) 如果 activity 沒有被回收,先強制進行一次 gc,再來檢查,如果 key 還存在 retainedKeys 中,說明 activity 不可回收,同時也說明了出現了記憶體洩漏。

2.4.3 FragmentRefWatcher#Helper#install()

public static void install(Context context, RefWatcher refWatcher) {
      List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();

      if (SDK_INT >= O) {
        // android sdk O 版本及其以上中的 fragment
        fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
      }

      try {
        // support 包中的 fragment
        Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
        Constructor<?> constructor =
            fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
        FragmentRefWatcher supportFragmentRefWatcher =
            (FragmentRefWatcher) constructor.newInstance(refWatcher);
        fragmentRefWatchers.add(supportFragmentRefWatcher);
      } catch (Exception ignored) {
      }

      if (fragmentRefWatchers.size() == 0) {
        return;
      }

      Helper helper = new Helper(fragmentRefWatchers);

      Application application = (Application) context.getApplicationContext();
// 監測 Activity 的生命週期。
application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }
複製程式碼

這段程式碼首先建立了一個 sdk 版本的 AndroidOFragmentRefWatcher,然後再建立一個 support 版本的 SupportFragmentRefWatcher,且它們都儲存在了 fragmentRefWatchers 列表中。最後新增了 Activity 生命週期的監聽,而監聽 callback 是 Helper 的 callback。

private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
        new ActivityLifecycleCallbacksAdapter() {
          @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            for (FragmentRefWatcher watcher : fragmentRefWatchers) {
              watcher.watchFragments(activity);
            }
          }
        };
複製程式碼

這裡只實現了 onActivityCreated(),而程式碼的邏輯就是當 Activity 建立時便遍歷所有的 FragmentRefWatcher 開始監測 fragment。以 AndroidOFragmentRefWatcher 為例來看一看。

@Override public void watchFragments(Activity activity) {
    FragmentManager fragmentManager = activity.getFragmentManager();
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
  }
複製程式碼

這裡有點巧妙了,即通過 FragmentManager 來註冊 fragment 的生命週期的回撥,再來看看這個 callback。

private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {

        @Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
          View view = fragment.getView();
          if (view != null) {
            refWatcher.watch(view);
          }
        }

        @Override
        public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
          refWatcher.watch(fragment);
        }
      };
複製程式碼

其監聽了 onFragmentViewDestroyed() 和 onFragmentDestroyed(),這兩個分別針對了 view 和 fragment 的監測是否存在洩漏。而後續的 watch 過程就和 Activity 是一樣的了。

與 Activity 的不同點在於對生週期的監聽實現,Fragment 的生命週期的監聽是通過 FragmentManager 來註冊實現的。其他 watch 的過程都是一樣的。

小結: 到這裡,所有的初始化工作已經完成了,等待接下來的記憶體洩漏的發生了。總的來說,這是一個抽絲剝繭的過程,只不過這個過程在這裡的分析過程中是一個逆向的:

(1) 我們先知道了當 heap hprof dump 完成後是由 DisplayLeakService 來處理的。

(2) 然後知道了當 heap hprof 分析完成後是由 ServiceHeapDumpListener 監聽到的,而交由 HeapAnalyzerService 進行分析的。

(3) 最後我們才知道通過監聽 Activity 與 Fragment 的生命週期,以及利用弱引用在 gc 後會被立即回收的特性來判斷是否有記憶體洩漏的發生。

3. dump heap hprof 檔案

前面的分析中已經知道,當 RefWatcher 監測到有記憶體洩漏後便會進行 heap dump 的操作以獲取當前記憶體的 heap hprof。這個程式碼的實現其實在前面分析的過程中已經見過了,即在 RefWatcher#ensureGone() 方法裡面,這裡再來仔細分析一下。

// 1. dump 出 heap hprof 檔案
File heapDumpFile = heapDumper.dumpHeap();
      ......
      // 2. 包裝成一個 HeapDump 物件
      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();
      // 3.交給 listener 做進一步處理
      heapdumpListener.analyze(heapDump);
複製程式碼

主要的過程就 3 個步驟,而根據前面的分析,這裡的 HeapDumper 就是 AndroidHeapDumper。那這裡的 dumpHeap 就是呼叫的它的實現了。

public File dumpHeap() {
    File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
    ......
    try {
     // dump hprof
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
      ......
      return heapDumpFile;
    } catch (Exception e) {
      ......
    }
  }
複製程式碼

看到了這裡的關鍵實現就是通過 Debug.dumpHprofData() 來 dump 出當前的記憶體 hprof 檔案。

最後,這個 hprof 檔案以及 KeyedWeakReference.key ,KeyedWeakReference.name 都會被封裝在 HeapDump 中,然後再將這個 HeapDump 經由 ServiceHeapDumpListener 和 HeapAnalyzerService 交給 leakcanary-analyzer 模組進行分析。那就來看看其是如何分析的吧。

4.分析 heap hprof

4.1 checkForLeak

heap dump 完之後如何監聽,如何交給 leakcanary-analyzer 的過程很簡單,就是啟動 HeapAnalyzerService,然後調起 HeapAnalyzer#checkForLeak() 方法進行 hprof 的分析。

/**
   * Searches the heap dump for a {@link KeyedWeakReference} instance with the corresponding key,
   * and then computes the shortest strong reference path from that instance to the GC roots.
   */
  public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
      @NonNull String referenceKey,
      boolean computeRetainedSize) {
    ......

    try {
      listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
      // 1.構建記憶體對映的 HprofBuffer,針對大檔案的一種快速的讀取方式,其原理是將檔案流的通道與  ByteBuffer 建立起關聯,並只在真正發生讀取時才從磁碟讀取內容出來。
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);  
     // 2.構造 Hprof 解析器
      HprofParser parser = new HprofParser(buffer);
      listener.onProgressUpdate(PARSING_HEAP_DUMP);
     // 3.獲取快照
      Snapshot snapshot = parser.parse();
      listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
      // 4.去重 gcRoots
      deduplicateGcRoots(snapshot);
      listener.onProgressUpdate(FINDING_LEAKING_REF);
      // 5.搜尋記憶體洩漏的索引
      Instance leakingRef = findLeakingReference(referenceKey, snapshot);
      ......
      // 6.搜尋索引鏈,並作為結果返回
      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }
複製程式碼

前面 3 個步驟中,通過 HprofBuffer 對 heap hprof 檔案進行記憶體對映,然後由 HprofParser 進行解析獲取 heap hrpof 的 snapshot,這一過程都是在呼叫 haha 庫進行的。其主要原理就是通過 hrpof 檔案協議進行解析,這裡就不深入展開其原理實現了,後面有機會再來分析 haha 庫的實現。

後 3 個步驟就是這裡要分析的重點。先來看看去重 gcRoots。

4.2 deduplicateGcRoots

/**
   * Pruning duplicates reduces memory pressure from hprof bloat added in Marshmallow.
   */
  void deduplicateGcRoots(Snapshot snapshot) {
    // THashMap has a smaller memory footprint than HashMap.
    final THashMap<String, RootObj> uniqueRootMap = new THashMap<>();
    final Collection<RootObj> gcRoots = snapshot.getGCRoots();
    for (RootObj root : gcRoots) {
      String key = generateRootKey(root);
      // 通過 root 來 generateRootKey,如果相同則去重
      if (!uniqueRootMap.containsKey(key)) {
        uniqueRootMap.put(key, root);
      }
    }
    // Repopulate snapshot with unique GC roots.
    gcRoots.clear();
    uniqueRootMap.forEach(new TObjectProcedure<String>() {
      @Override public boolean execute(String key) {
        // 把不相同的全部新增到 gcRoots 中
        return gcRoots.add(uniqueRootMap.get(key));
      }
    });
  }
複製程式碼

這裡主要關心的是 generateRootKey() 的實現。

  private String generateRootKey(RootObj root) {
    return String.format("%s@0x%08x", root.getRootType().getName(), root.getId());
  }
複製程式碼

物件的型別以及ID,是由虛擬機器 ART 分配的,一般來說在一個程式裡肯定是唯一的。再來繼續看 findLeakTrace()。

4.3 findLeakingReference

private Instance findLeakingReference(String key, Snapshot snapshot) {
    // 搜尋類 KeyedWeakReference 
    ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
    ......
    List<String> keysFound = new ArrayList<>();
    // 遍歷KeyedWeakReference類的所有例項物件
    for (Instance instance : refClass.getInstancesList()) {
     // 獲取物件的所有屬性
      List<ClassInstance.FieldValue> values = classInstanceValues(instance);
      // 看其是否包含有屬性 key
      Object keyFieldValue = fieldValue(values, "key");
     // 對於沒有統一設定為 null
      if (keyFieldValue == null) {
        keysFound.add(null);
        continue;
      }
      // 找到了就作為候選例項的屬性
      String keyCandidate = asString(keyFieldValue);
     // 如果該屬性值與 key 相等,就認為是找到了
      if (keyCandidate.equals(key)) {
        return fieldValue(values, "referent");
      }
      keysFound.add(keyCandidate);
    }
    ......
  }
複製程式碼

這段程式碼的意思就是通過搜尋 KeyedWeakReference 這個類的所有例項物件,然後看其物件中的 key 的值與所要搜尋的物件的 key 的值是否相等,相等則為要所找到的物件的引用。這個 key 是什麼呢?這個 key 就是前面在 RefWatcher#watch() 中通過 UUID 所構造出來的。找到索引後,接下來就是搜尋其索引鏈了。

4.4 findLeakTrace

private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
      Instance leakingRef, boolean computeRetainedSize) {
    listener.onProgressUpdate(FINDING_SHORTEST_PATH);
    // 尋找最短路徑
    ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
    // False alarm, no strong reference path to GC Roots.
    if (result.leakingNode == null) {
      return noLeak(since(analysisStartNanoTime));
    }
    listener.onProgressUpdate(BUILDING_LEAK_TRACE);
    // 構建 LeakTrace
    LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
    String className = leakingRef.getClassObj().getClassName();
    // 計算 retained size
    long retainedSize;
    if (computeRetainedSize) {

      listener.onProgressUpdate(COMPUTING_DOMINATORS);
      // Side effect: computes retained size.
      snapshot.computeDominators();

      Instance leakingInstance = result.leakingNode.instance;

      retainedSize = leakingInstance.getTotalRetainedSize();

      // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
      if (SDK_INT <= N_MR1) {
        listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
        retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
      }
    } else {
      retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
    }
    // 構建最終的結果 AnalysisResult
    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
  }
複製程式碼

這裡主要的是 4 個步驟,尋找最短路徑,計算 retained size,構建 LeakTrace 以及構建 AnalysisResult。這些都是 haha 庫對其是如何解析的,這裡就不再繼續分析下去了。因為這裡到了另一個大的話題,需要虛擬機器相關的知識,尤其是 heap 管理這一塊,以及需要熟知 hprof 協議。所以,這裡更詳細的分析將放在下一篇分析 haha 庫相關的文章裡面去。

四、總結

(1) 從大的層面上來看,LeakCanary 的核心包括了用於監測記憶體洩漏的 leakcanary-watcher 以及用於分析 hprof 的 leakcanary-analyzer 兩個大的模組。leakcanary-watcher 中的核心類是 RefWatcher ,而 leakcanary-analyzer 中的核心類是 HeapAnalyzer。對於應用開發者來說,掌握 leakcanary-watcher 的原理會更適用一些。

(2) leakcanary-watcher 監測記憶體的核心原理是,監測 Activity/Fragment 元件的生命週期,元件在進入 onDestroy 後,一般的 framework 會主動 gc 一次,如果沒有則還會強制 gc 一次。同時,還利用了弱引用在一次 gc 後便會進入到記憶體回收的狀態的原理從而判斷當前被監測的元件是否發生了記憶體洩漏。

(3) leakcanary-analyzer 負責 hprof 的分析,這個模組其實只是對 haha 庫的所提供的 api 的應用,就像我們拿著 leakcanary 編寫自己的業務程式碼一樣。這裡不作深入的分析,而是放到下一篇 haha 庫相關的文章裡再來詳細分析。

最後,感謝你能讀到並讀完此文章,如果分析的過程中存在錯誤或者疑問都歡迎留言討論。如果我的分享能夠幫助到你,還請記得幫忙點個贊吧,謝謝。

相關文章