007 LeakCanary 記憶體洩漏原理完全解析

orzangleli發表於2019-03-03

LeakCanary 的工作原理是什麼?跟我一起揭開它的神祕面紗。

一、 什麼是LeakCanary

LeakCanary 是大名鼎鼎的 square 公司開源的記憶體洩漏檢測工具。目前上大部分App在開發測試階段都會接入此工具用於檢測潛在的記憶體洩漏問題,做的好一點的可能會搭建一個伺服器用於儲存各個裝置上的記憶體洩漏問題再集中處理。

本文首發於我的微信公眾號:Android開發實驗室,歡迎大家關注和我一起學Android,掉節操。

二、 為什麼要使用LeakCanary

我們知道記憶體洩漏問題的排查有很多種方法, 比如說,Android Studio 自帶的 Profile 工具、MAT(Memory Analyzer Tool)、以及LeakCanary。 選擇 LeakCanary 作為首選的記憶體洩漏檢測工具主要是因為它能實時檢測洩漏並以非常直觀的呼叫鏈方式展示記憶體洩漏的原因。

三、 LeakCanary 做不到的(待定)

雖然 LeakCanary 有諸多優點,但是它也有做不到的地方,比如說檢測申請大容量記憶體導致的OOM問題、Bitmap記憶體未釋放問題,Service 中的記憶體洩漏可能無法檢測等。

四、 LeakCanary 原始碼解析

本章內容前後依賴關係強烈,建議順序閱讀。

4.1 ActivityLifecycleCallbacks 與 FragmentLifeCycleCallbacks

在開始 LeakCanary 原理解析之前,有必要簡單說下 ActivityLifecycleCallbacks 與 FragmentLifeCycleCallbacks。

// ActivityLifecycleCallbacks 介面
public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity var1, Bundle var2);

    void onActivityStarted(Activity var1);

    void onActivityResumed(Activity var1);

    void onActivityPaused(Activity var1);

    void onActivityStopped(Activity var1);

    void onActivitySaveInstanceState(Activity var1, Bundle var2);

    void onActivityDestroyed(Activity var1);
  }
複製程式碼

Application 類提供了 registerActivityLifecycleCallbacksunregisterActivityLifecycleCallbacks 方法用於註冊和反註冊 Activity 的生命週期監聽類,這樣我們就能在 Application 中對所有的 Activity 生命週期回撥中做一些統一處理。

public abstract static class FragmentLifecycleCallbacks {

    public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {}

    public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {}

    public void onFragmentDestroyed(FragmentManager fm, Fragment f) {}

    // 省略其他的生命週期 ...
  }
複製程式碼

FragmentManager 類提供了 registerFragmentLifecycleCallbacksunregisterFragmentLifecycleCallbacks 方法使用者註冊和反註冊 Fragment 的生命週期監聽類,這樣我們對每一個 Activity 進行註冊,就能獲取所有的 Fragment 生命週期回撥。

4.2 LeakCanary 的使用

4.2.1 使用方法

我們直接在 Application 類中,新增一下程式碼即可。

public class ExampleApplication extends Application {
  @Override public void onCreate() {
    super.onCreate();
    setupLeakCanary();
  }

  protected void setupLeakCanary() {
    // 啟用嚴格模式
    enabledStrictMode();
    // 判斷是否是 HeapAnalyzerService 所屬程式
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    // 註冊 LeakCanary
    LeakCanary.install(this);
  }

  private static void enabledStrictMode() {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() //
        .detectAll() //
        .penaltyLog() //
        .penaltyDeath() //
        .build());
  }
}
複製程式碼
<service
    android:name=".internal.HeapAnalyzerService"
    android:process=":leakcanary"
    android:enabled="false"
    />
複製程式碼

由於 LeakCanary 的核心 hropf 檔案解析服務 HeapAnalyzerService 所屬程式是與主程式獨立的一個程式,所以在 setupLeakCanary中,我們需要排除其他程式,只對 leakcanary 程式註冊 LeakCanary 監聽處理。

android:enabled=”false” 這是什麼?
這裡簡單說下,AndroidManifest檔案中的 enabled 屬性,可以看到 HeapAnalyzerService 這個元件預設是不可用的,所以如果在程式碼中動態啟用這個元件,可以使用以下方法:

public static void setEnabledBlocking(Context appContext, Class<?> componentClass,
      boolean enabled) {
  ComponentName component = new ComponentName(appContext, componentClass);
  PackageManager packageManager = appContext.getPackageManager();
  int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
  // Blocks on IPC.
  packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
}
複製程式碼

4.3 LeakCanary.install(this) 幹了什麼

LeakCanary 的 install 方法實際上構造了一個 RefWatcher,

/**
   * 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 refWatcher(application).listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      .buildAndInstall();
}
複製程式碼

我們一個個來看這個註冊方法。首先是 refWatcher 方法構造了一個 AndroidRefWatcherBuilder, 傳入引數是當前Application 的 Context.

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

listenerServiceClass 和 excludedRefs 方法是基於建造者模式傳入分析Service 和 排除已知的洩漏問題 AndroidExcludedRefs,這裡我就不貼程式碼了。

重點看下 buildAndInstall 方法,這個方法很形象的表示將要進行建造者模式的最後一步 build 和 註冊一些監聽器,下面我們來看具體程式碼:

public @NonNull RefWatcher buildAndInstall() {
  // 只允許 install 一次
  if (LeakCanaryInternals.installedRefWatcher != null) {
    throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
  }
  // 建造者模式的最後一步,構造物件
  RefWatcher refWatcher = build();
  // 判斷是否開啟了 LeakCanary,沒有開啟預設會返回 DISABLED 物件
  if (refWatcher != DISABLED) {
    // 手動開啟 DisplayLeakActivity 元件,會在桌面上顯示一個檢視記憶體洩漏結果的入口
    LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
    // 是否檢測 Activity 的 記憶體洩漏,預設開啟
    if (watchActivities) {
      ActivityRefWatcher.install(context, refWatcher);
    }
    // 是否檢測 Fragment 的 記憶體洩漏,預設開啟
    if (watchFragments) {
      FragmentRefWatcher.Helper.install(context, refWatcher);
    }
  }
  // 複製給全域性靜態變數,防止二次呼叫
  LeakCanaryInternals.installedRefWatcher = refWatcher;
  return refWatcher;
}
複製程式碼

以上程式碼作用大部分都在程式碼中註釋了,剩下 ActivityRefWatcher.install 和 FragmentRefWatcher.Helper.install 方法沒有註釋。下面我們就來具體看看這兩個方法究竟幹了什麼。

(1). ActivityRefWatcher.install

ActivityRefWatcher 的靜態方法 install 獲取到了當前 Application,然後新增了一個生命週期監聽器 ActivityLifecycleCallbacks,這裡的 lifecycleCallbacks 僅僅關注了 Activity 銷燬的回撥 onActivityDestroyed,在這裡將傳入的物件 activity 監聽起來, refWatcher.watch(activity); 的具體程式碼我們稍後分析。

public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
 Application application = (Application) context.getApplicationContext();
 ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

 application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}

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

(2). FragmentRefWatcher.Helper.install
FragmentRefWatcher.Helper 的靜態方法 install 裡同樣會註冊一個 ActivityLifecycleCallbacks 用於監聽 Activity 生命週期中的 onActivityCreated 的建立完成的回撥,在 Activity 建立完成後,會對這個 Activity 註冊 Fragment 的生命週期監聽器。install 方法首先會判斷系統是否大於等於 Android O, 如果是那麼會使用 android.app.FragmentManager 進行註冊,如果需要相容 Android O 以下需要自行在依賴中新增對 leakcanary-support-fragment 元件的依賴,然後通過反射構造出SupportFragmentRefWatcher; 然後將fragmentRefWatchers所有監聽器取出,在 Activity 建立完成後,新增 Fragment 的生命監聽,主要關注 Fragment 的 onFragmentViewDestroyedonFragmentDestroyed 方法。具體程式碼如下:

public static void install(Context context, RefWatcher refWatcher) {
      List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();
      // 系統是否大於等於 Android O,如果是,新增 AndroidOFragmentRefWatcher
      if (SDK_INT >= O) {
        fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
      }
      // 如果新增了leakcanary-support-fragment的依賴,通過反射可以構造SupportFragmentRefWatcher
      try {
        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);
      // 先監聽 Activity 的建立完成回撥
      Application application = (Application) context.getApplicationContext();
      application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }

    private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
          // Activity 建立完成後,對Activity中的Fragment註冊生命週期監聽
          for (FragmentRefWatcher watcher : fragmentRefWatchers) {
            watcher.watchFragments(activity);
          }
        }
    };
複製程式碼
// AndroidOFragmentRefWatcher.java

private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
  new FragmentManager.FragmentLifecycleCallbacks() {
    // Fragment 中的View 檢視銷燬時
    @Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
      View view = fragment.getView();
      if (view != null) {
        refWatcher.watch(view);
      }
    }
    // Fragment 銷燬時
    @Override
    public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
      refWatcher.watch(fragment);
    }
  };

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

上述流程我們已經完全搞清楚了,用一個流程圖可以表示為:

LeakCanary註冊流程

4.4 LeakCanary 記憶體洩漏檢測原理

從行文結構上,本小節應屬於上一節後半部分內容,但是RefWatcher 的 watch 方法足夠重要和複雜,所以有必要單獨列一節仔細講解內部原理。

4.4.1 基礎知識——弱引用 WeakReference 和 引用佇列 ReferenceQueue

關於引用型別和引用佇列相關知識,讀者可以參考白話 JVM——深入物件引用,這篇文章我認為講解的比較清晰。

這裡,我簡單舉個例子,弱引用在定義的時候可以指定引用物件和一個 ReferenceQueue,弱引用物件在垃圾回收器執行回收方法時,如果原物件只有這個弱引用物件引用著,那麼會回收原物件,並將弱引用物件加入到 ReferenceQueue,通過 ReferenceQueue 的 poll 方法,可以取出這個弱引用物件,獲取弱引用物件本身的一些資訊。看下面這個例子。

mReferenceQueue = new ReferenceQueue<>();
// 定義一個物件
o = new Object();
// 定義一個弱引用物件引用 o,並指定引用佇列為 mReferenceQueue
weakReference = new WeakReference<Object>(o, mReferenceQueue);
// 去掉強引用
o = null;
// 觸發應用進行垃圾回收
Runtime.getRuntime().gc();
// hack: 延時100ms,等待gc完成
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}
Reference ref = null;
// 遍歷 mReferenceQueue,取出所有弱引用
while ((ref = mReferenceQueue.poll()) != null) {
    System.out.println("============ 
 ref in queue");
}
複製程式碼

列印結果為:

============
ref in queue

4.4.2 基礎知識——hprof檔案

hprof 檔案可以展示某一時刻java堆的使用情況,根據這個檔案我們可以分析出哪些物件佔用大量記憶體和未在合適時機釋放,從而定位記憶體洩漏問題。

Android 生成 hprof 檔案整體上有兩種方式:

  1. 使用 adb 命令
adb shell am dumpheap <processname> <FileName>
複製程式碼
  1. 使用 android.os.Debug.dumpHprofData 方法
    直接使用 Debug 類提供的 dumpHprofData 方法即可。
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
複製程式碼

Android Studio 自帶 Android Profiler 的 Memory 模組的 dump 操作使用的是方法一。這兩種方法生成的 .hprof 檔案都是 Dalvik 格式,需要使用 AndroidSDK 提供的 hprof-conv 工具轉換成J2SE HPROF格式才能在MAT等標準 hprof 工具中檢視。

hprof-conv dump.hprof converted-dump.hprof  
複製程式碼

至於hprof內部格式如何,本文不做具體介紹,以後有機會再單獨寫一篇文章來仔細講解。LeakCanary 解析 .hprof 檔案用的是 square 公司開源的另一專案:haha.

4.4.3 watch方法

終於到了 LeakCanary 關鍵部分了。我們從 watch 方法入手,前面的程式碼都是為了增強魯棒性,我們直接從生成唯一id開始,LeakCanary 構造了一個帶有 key 的弱引用物件,並且將 queue 設定為弱引用物件的引用佇列。

這裡解釋一下,為什麼需要建立一個帶有 key 的弱引用物件,不能直接使用 WeakReference 麼?
舉個例子,假設 OneActivity 發生了記憶體洩漏,那麼執行 GC 操作時,肯定不會回收 Activity 物件,這樣 WeakReference 物件也不會被回收。假設當前啟動了 N 個 OneActivity,Dump記憶體時我們可以獲取到記憶體中的所有 OneActivity,但是當我們準備去檢測其中某一個 Activity 的洩漏問題時,我們就無法匹配。但是如果使用了帶有 key 的 WeakReference 物件,發生洩露時洩漏時,key 的值也會 dump 儲存下來,這樣我們根據 key 的一一對應關係就能對映到某一個 Activity。

然後,LeakCanary 呼叫了 ensureGoneAsync 方法去檢測記憶體洩漏。

public void watch(Object watchedReference, String referenceName) {
  if (this == DISABLED) {
    return;
  }
  checkNotNull(watchedReference, "watchedReference");
  checkNotNull(referenceName, "referenceName");
  final long watchStartNanoTime = System.nanoTime();
  // 對當前監視物件設定一個唯一 id
  String key = UUID.randomUUID().toString();
  // 新增到 Set<String> 中
  retainedKeys.add(key);
  // 構造一個帶有id 的 WeakReference 物件
  final KeyedWeakReference reference =
      new KeyedWeakReference(watchedReference, key, referenceName, queue);
  // 檢測物件是否被回收了
  ensureGoneAsync(watchStartNanoTime, reference);
}
複製程式碼

4.4.4 ensureGoneAsync 方法

ensureGoneAsync 方法構造了一個 Retryable 物件,並將它傳給 watchExecutor 的 execute 方法。

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
  // watchExecutor 是 AndroidWatchExecutor的一個例項
  watchExecutor.execute(new Retryable() {
    @Override public Retryable.Result run() {
      return ensureGone(reference, watchStartNanoTime);
    }
  });
}
複製程式碼

watchExecutor 是 AndroidWatchExecutor 的一個例項, AndroidWatchExecutor 的 execute 方法的作用就是判斷當前執行緒是否是主執行緒,如果是主執行緒,那麼直接執行 waitForIdle 方法,否則通過 Handler 的 post 方法切換到主執行緒再執行 waitForIdle 方法。

@Override public void execute(@NonNull Retryable retryable) {
  // 判斷當前執行緒是否是主執行緒
  if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
    waitForIdle(retryable, 0);
  } else {
    postWaitForIdle(retryable, 0);
  }
}
複製程式碼

waitForIdle 方法通過呼叫 addIdleHandler 方法,指定當主程式中沒有需要處理的事件時,在這個空閒期間執行 postToBackgroundWithDelay 方法。

private void waitForIdle(final Retryable retryable, final int failedAttempts) {
  // 由於上面的 execute 方法,已經保證了此方法在主執行緒中執行,所以Looper.myQueue()獲取的主執行緒的訊息佇列
  Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override public boolean queueIdle() {
      postToBackgroundWithDelay(retryable, failedAttempts);
      // return false 表示執行完之後,就立即移除這個事件
      return false;
    }
  });
}
複製程式碼

postToBackgroundWithDelay 方法首先會計算延遲時間 delayMillis,這個延時是有 exponentialBackoffFactor(指數因子) 乘以初始延時時間得到的, exponentialBackoffFactor(指數因子)會在2^n 和 Long.MAX_VALUE / initialDelayMillis 中取較小值,也就說延遲時間delayMillis = initialDelayMillis * 2^n,且不能超過 Long.MAX_VALUE。

private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
   long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
   // 計算延遲時間
   long delayMillis = initialDelayMillis * exponentialBackoffFactor;
   // 切換到子執行緒中執行
   backgroundHandler.postDelayed(new Runnable() {
     @Override public void run() {
       // 執行 retryable 裡的 run 方法
       Retryable.Result result = retryable.run();
       // 如果需要重試,那麼再新增到主執行緒的空閒期間執行
       if (result == RETRY) {
         postWaitForIdle(retryable, failedAttempts + 1);
       }
     }
   }, delayMillis);
}
複製程式碼

postToBackgroundWithDelay 方法每次執行會指數級增加延時時間,延時時間到了後,會執行 Retryable 裡的方法,如果返回為重試,那麼會增加延時時間並執行下一次。

retryable.run() 的run 方法又執行了什麼呢?別忘了我們ensureGoneAsync中的程式碼,一直在重試的程式碼正式 ensureGone 方法。

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

4.4.5 ensureGone 方法

我現在講ensureGone方法的完整程式碼貼出來,我們逐行分析:

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
  long gcStartNanoTime = System.nanoTime();
  // 前面不是有一個重試的機制麼,這裡會計下這次重試距離第一次執行花了多長時間
  long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
  // 移除所有弱引用可達物件,後面細講
  removeWeaklyReachableReferences();
  // 判斷當前是否正在開啟USB除錯,LeakCanary 的解釋是除錯時可能會觸發不正確的記憶體洩漏
  if (debuggerControl.isDebuggerAttached()) {
    // The debugger can create false leaks.
    return RETRY;
  }
  // 上面執行 removeWeaklyReachableReferences 方法,判斷是不是監視物件已經被回收了,如果被回收了,那麼說明沒有發生記憶體洩漏,直接結束
  if (gone(reference)) {
    return DONE;
  }
  // 手動觸發一次 GC 垃圾回收
  gcTrigger.runGc();
  // 再次移除所有弱引用可達物件
  removeWeaklyReachableReferences();
  // 如果物件沒有被回收
  if (!gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
    // 使用 Debug 類 dump 當前堆記憶體中物件使用情況
    File heapDumpFile = heapDumper.dumpHeap();
    // dumpHeap 失敗的話,會走重試機制
    if (heapDumpFile == RETRY_LATER) {
      // Could not dump the heap.
      return RETRY;
    }
    long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
    // 將hprof檔案、key等屬性構造一個 HeapDump 物件
    HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
        .referenceName(reference.name)
        .watchDurationMs(watchDurationMs)
        .gcDurationMs(gcDurationMs)
        .heapDumpDurationMs(heapDumpDurationMs)
        .build();
    // heapdumpListener 分析 heapDump 物件
    heapdumpListener.analyze(heapDump);
  }
  return DONE;
}
複製程式碼

看完上述程式碼,基本把檢測洩漏的大致過程走了一遍,下面我們來看一些具體的細節。

(1). removeWeaklyReachableReferences 方法

removeWeaklyReachableReferences 移除所有弱引用可達物件是怎麼工作的?

private void removeWeaklyReachableReferences() {
  KeyedWeakReference ref;
  while ((ref = (KeyedWeakReference) queue.poll()) != null) {
    retainedKeys.remove(ref.key);
  }
}
複製程式碼

還記得我們在 refWatcher.watch 方法儲存了當前監視物件的 ref.key 了麼,如果這個物件被回收了,那麼對應的弱引用物件會在回收時被新增到queue中,通過 poll 操作就可以取出這個弱引用,這時候我們從retainedKeys中移除這個 key, 代表這個物件已經被正常回收,不需要再被監視了。

那麼現在來看,判斷這個物件是否被回收就比較簡單了?

private boolean gone(KeyedWeakReference reference) {
  // retainedKeys 中不包含 reference.key 的話,就代表這個物件已經被回收了
  return !retainedKeys.contains(reference.key);
}
複製程式碼

(2). dumpHeap 方法

heapDumper.dumpHeap() 是執行生成hprof的方法,heapDumper 是 AndroidHeapDumper 的一個物件,我們來具體看看它的 dump 方法。

public File dumpHeap() {
  // 生成一個儲存 hprof 的檔案
  File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
  // 檔案建立失敗
  if (heapDumpFile == RETRY_LATER) {
    return RETRY_LATER;
  }
  // FutureResult 內部有一個 CountDownLatch,用於倒數計時
  FutureResult<Toast> waitingForToast = new FutureResult<>();
  // 切換到主執行緒顯示 toast
  showToast(waitingForToast);
  // 等待5秒,確保 toast 已完成顯示
  if (!waitingForToast.wait(5, SECONDS)) {
    CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
    return RETRY_LATER;
  }
  // 建立一個通知
  Notification.Builder builder = new Notification.Builder(context)
      .setContentTitle(context.getString(R.string.leak_canary_notification_dumping));
  Notification notification = LeakCanaryInternals.buildNotification(context, builder);
  NotificationManager notificationManager =
      (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
  int notificationId = (int) SystemClock.uptimeMillis();
  notificationManager.notify(notificationId, notification);

  Toast toast = waitingForToast.get();
  try {
    // 開始 dump 記憶體到指定檔案
    Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
    cancelToast(toast);
    notificationManager.cancel(notificationId);
    return heapDumpFile;
  } catch (Exception e) {
    CanaryLog.d(e, "Could not dump heap");
    // Abort heap dump
    return RETRY_LATER;
  }
}
複製程式碼

這段程式碼裡我們需要看看 showToast() 方法,以及它是如何確保 toast 已完成顯示(有點黑科技的感覺)。

private void showToast(final FutureResult<Toast> waitingForToast) {
  mainHandler.post(new Runnable() {
    @Override public void run() {
      // 當前 Activity 已經 paused的話,直接返回
      if (resumedActivity == null) {
        waitingForToast.set(null);
        return;
      }
      // 構建一個toast 物件
      final Toast toast = new Toast(resumedActivity);
      toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
      toast.setDuration(Toast.LENGTH_LONG);
      LayoutInflater inflater = LayoutInflater.from(resumedActivity);
      toast.setView(inflater.inflate(R.layout.leak_canary_heap_dump_toast, null));
      // 將toast加入顯示佇列
      toast.show();
      // Waiting for Idle to make sure Toast gets rendered.
      // 主執行緒中新增空閒時操作,如果主執行緒是空閒的,會將CountDownLatch執行 countDown 操作
      Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override public boolean queueIdle() {
          waitingForToast.set(toast);
          return false;
        }
      });
    }
  });
}
複製程式碼

首先我們需要知道所有的Toast物件,並不是我們一呼叫 show 方法就立即顯示的。NotificationServiceManager會從mToastQueue中輪詢去除Toast物件進行顯示。如果Toast的顯示不是實時的,那麼我們如何知道Toast是否已經顯示完成了呢?我們在 Toast 呼叫 show 方法後呼叫 addIdleHandler, 在主程式空閒時執行 CountDownLatch 的減一操作。由於我們知道我們順序加入到主執行緒的訊息佇列中的操作:先顯示Toast,再執行 CountDownLatch 減一操作。所以在 if (!waitingForToast.wait(5, SECONDS)) 的判斷中,我們最多等待5秒,如果超時會走重試機制,如果我們的 CountDownLatch 已經執行了減一操作,則會正常走後續流程,同時我們也能推理出它前面 toast 肯定已經顯示完成了。

Debug.dumpHprofData(heapDumpFile.getAbsolutePath());是系統Debug類提供的方法,我就不做具體分析了。

(3). heapdumpListener.analyze(heapDump) 方法

heapdumpListener 是 ServiceHeapDumpListener 的一個物件,最終執行了HeapAnalyzerService.runAnalysis方法。

@Override public void analyze(@NonNull HeapDump heapDump) {
  checkNotNull(heapDump, "heapDump");
  HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}
複製程式碼
// 啟動前臺服務
public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
  setEnabledBlocking(context, HeapAnalyzerService.class, true);
  setEnabledBlocking(context, listenerServiceClass, true);
  Intent intent = new Intent(context, HeapAnalyzerService.class);
  intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
  intent.putExtra(HEAPDUMP_EXTRA, heapDump);
  ContextCompat.startForegroundService(context, intent);
}
複製程式碼

HeapAnalyzerService 繼承自 IntentService,IntentService的具體原理我就不多做解釋了。IntentService會將所有併發的啟動服務操作,變成順序執行 onHandleIntent 方法。

@Override protected void onHandleIntentInForeground(@Nullable Intent intent) {
  if (intent == null) {
    CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
    return;
  }
  // 監聽 hprof 檔案分析結果的類
  String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
  // hprof 檔案類
  HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

  HeapAnalyzer heapAnalyzer =
      new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
  // checkForLeak 會呼叫 haha 元件中的工具,分析 hprof 檔案
  AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
      heapDump.computeRetainedHeapSize);
  // 將分析結果傳送給監聽器 listenerClassName
  AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
複製程式碼

我們來看下 checkForLeak 方法,我們一起來看下吧。

public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
      @NonNull String referenceKey,
      boolean computeRetainedSize) {
  long analysisStartNanoTime = System.nanoTime();
  // 檔案不存在的話,直接返回
  if (!heapDumpFile.exists()) {
    Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
    return failure(exception, since(analysisStartNanoTime));
  }

  try {
    // 更新進度回撥
    listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
    // 將 hprof 檔案解析成 Snapshot
    HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
    HprofParser parser = new HprofParser(buffer);
    listener.onProgressUpdate(PARSING_HEAP_DUMP);
    Snapshot snapshot = parser.parse();
    listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
    // 移除相同 GC root項
    deduplicateGcRoots(snapshot);
    listener.onProgressUpdate(FINDING_LEAKING_REF);
    // 查詢記憶體洩漏項
    Instance leakingRef = findLeakingReference(referenceKey, snapshot);

    // False alarm, weak reference was cleared in between key check and heap dump.
    // 沒有找到,就代表沒有洩漏
    if (leakingRef == null) {
      return noLeak(since(analysisStartNanoTime));
    }
    // 找到洩漏處的引用關係鏈
    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
  } catch (Throwable e) {
    return failure(e, since(analysisStartNanoTime));
  }
}
複製程式碼

hprof 檔案的解析是由開源專案 haha 完成的,我這裡不做過多展開。

findLeakingReference 方法是查詢洩漏的引用處,我們看下程式碼:

private Instance findLeakingReference(String key, Snapshot snapshot) {
  // 從 hprof 檔案儲存的物件中找到所有 KeyedWeakReference 的例項
  ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
  if (refClass == null) {
    throw new IllegalStateException(
        "Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
  }
  List<String> keysFound = new ArrayList<>();
  // 對 KeyedWeakReference 例項列表進行遍歷
  for (Instance instance : refClass.getInstancesList()) {
    // 獲取每個例項裡的所有欄位
    List<ClassInstance.FieldValue> values = classInstanceValues(instance);
    // 找到 key 欄位對應的值
    Object keyFieldValue = fieldValue(values, "key");
    if (keyFieldValue == null) {
      keysFound.add(null);
      continue;
    }
    // 將 keyFieldValue 轉為 String 物件
    String keyCandidate = asString(keyFieldValue);
    // 如果這個物件的 key 和 我們查詢的 key 相同,那麼返回這個弱物件持有的原物件
    if (keyCandidate.equals(key)) {
      return fieldValue(values, "referent");
    }
    keysFound.add(keyCandidate);
  }
  throw new IllegalStateException(
      "Could not find weak reference with key " + key + " in " + keysFound);
}
複製程式碼

到現在為止,我們已經把 LeakCanary 檢測記憶體洩漏的全部過程的原始碼看完了。個人認為 LeakCanary 原始碼寫的不錯,可讀性很高,查詢呼叫關係也比較方便(這裡黑一下 bilibili 的 DanmakusFlameMaster)。

五. 版權宣告

本文首發於我的微信公眾號:Android開發實驗室,歡迎大家關注和我一起學Android,掉節操。未經允許,不得轉載。

https://user-gold-cdn.xitu.io/2018/12/3/16774bbd97cdf7a8?w=258&h=258&f=jpeg&s=27337

相關文章