1. 程式人生 > >LeakCanary原始碼探討- 如何檢測 Activity 是否洩漏

LeakCanary原始碼探討- 如何檢測 Activity 是否洩漏

OOM 是 Android 開發中常見的問題,而記憶體洩漏往往是罪魁禍首。

為了簡單方便的檢測記憶體洩漏,Square 開源了 LeakCanary,它可以實時監測 Activity 是否發生了洩漏,一旦發現就會自動彈出提示及相關的洩漏資訊供分析。

本文的目的是試圖通過分析 LeakCanary 原始碼來探討它的 Activity 洩漏檢測機制。

LeakCanary 使用方式

為了將 LeakCanary 引入到我們的專案裡,我們只需要做以下兩步:


     dependencies {
     debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' }

    public class ExampleApplication extends Application {
      @Override public void onCreate() {
      super.onCreate();
     if (LeakCanary.isInAnalyzerProcess(this
)) { // This process is dedicated to LeakCanary for heap analysis. // You should not init your app in this process. return; } LeakCanary.install(this); } }

可以看出,最關鍵的就是 LeakCanary.install(this); 這麼一句話,正式開啟了 LeakCanary 的大門,未來它就會自動幫我們檢測記憶體洩漏,並在發生洩漏是彈出通知資訊。

從 LeakCanary.install(this); 開始

下面我們來看下它做了些什麼?

 public static RefWatcher install(Application application) {
  return install(application, DisplayLeakService.class,
  AndroidExcludedRefs.createAppDefaults().build());
}

public static RefWatcher install(Application application,
Class<? extends AbstractAnalysisResultService> listenerServiceClass,
ExcludedRefs excludedRefs) {
   if (isInAnalyzerProcess(application)) {
      return RefWatcher.DISABLED;
   }
  enableDisplayLeakActivity(application);
  HeapDump.Listener heapDumpListener =
  new ServiceHeapDumpListener(application, listenerServiceClass);
  RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
  ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
  return refWatcher;
}

首先,我們先看最重要的部分,就是:

  RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
ActivityRefWatcher.installOnIcsPlus(application, refWatcher);

先生成了一個 RefWatcher,這個東西非常關鍵,從名字可以看出,它是用來 watch Reference 的,也就是用來一個監控引用的工具。然後再把 refWatcher 和我們自己提供的 application 傳入到 ActivityRefWatcher.installOnIcsPlus(application, refWatcher); 這句裡面,繼續看。

 public static void installOnIcsPlus(Application application,   RefWatcher refWatcher) {
         ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
         activityRefWatcher.watchActivities();
}

建立了一個 ActivityRefWatcher,大家應該能感受到,這個東西就是用來監控我們的 Activity 洩漏狀況的,它呼叫watchActivities() 方法,就可以開始進行監控了。下面就是它監控的核心原理:

 public void watchActivities() {
  application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
}

它向 application 裡註冊了一個 ActivitylifecycleCallbacks 的回撥函式,可以用來監聽 Application 整個生命週期所有 Activity 的 lifecycle 事件。再看下這個 lifecycleCallbacks 是什麼?

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =

 new Application.ActivityLifecycleCallbacks() {
  @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
  }

  @Override public void onActivityStarted(Activity activity) {
  }

  @Override public void onActivityResumed(Activity activity) {
  }

  @Override public void onActivityPaused(Activity activity) {
  }

  @Override public void onActivityStopped(Activity activity) {
  }

  @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
  }

  @Override public void onActivityDestroyed(Activity activity) {
    ActivityRefWatcher.this.onActivityDestroyed(activity);
  }
};

原來它只監聽了所有 Activity 的 onActivityDestroyed 事件,當 Activity 被 Destory 時,呼叫 ActivityRefWatcher.this.onActivityDestroyed(activity); 函式。

猜測下,正常情況下,當一個這個函式應該 activity 被 Destory 時,那這個 activity 物件應該變成 null 才是正確的。如果沒有變成 null,那麼就意味著發生了記憶體洩漏。

因此我們向,這個函式 ActivityRefWatcher.this.onActivityDestroyed(activity); 應該是用來監聽 activity 物件是否變成了 null。繼續看。

void onActivityDestroyed(Activity activity) {
  refWatcher.watch(activity);
}

RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);

可以看出,這個函式把目標 activity 物件傳給了RefWatcher,讓它去監控這個 activity 是否被正常回收了,若未被回收,則意味著發生了記憶體洩漏。

RefWatcher 如何監控activity是否被正常回收呢?

我們先來看看這個RefWatcher究竟是個什麼東西?

 public static RefWatcher androidWatcher(Context context, HeapDump.Listener heapDumpListener,
  ExcludedRefs excludedRefs) {
  AndroidHeapDumper heapDumper = new AndroidHeapDumper(context,    leakDirectoryProvider);
   heapDumper.cleanup();

    int watchDelayMillis = 5000;
    AndroidWatchExecutor executor = new AndroidWatchExecutor(watchDelayMillis);

    return new RefWatcher(executor, debuggerControl, GcTrigger.DEFAULT, heapDumper,
    heapDumpListener, excludedRefs);
}

這裡面涉及到兩個新的物件:AndroidHeapDumperAndroidWatchExecutor,前者用來 dump 堆記憶體狀態的,後者則是用來 watch 一個引用的監聽器。具體原理後面再看。總之,這裡已經生成好了一個 RefWatcher 物件了。

現在再看上面onActivityDestroyed(Activity activity) 裡呼叫的 refWatcher.watch(activity);,下面來看下這個最為核心的 watch(activity) 方法,瞭解它是如何監控 activity 是否被回收的。

private final Set<String> retainedKeys;
public void watch(Object activity, String referenceName) {
  String key = UUID.randomUUID().toString();
  retainedKeys.add(key);

  final KeyedWeakReference reference =
  new KeyedWeakReference(activity, key, referenceName, queue);

  watchExecutor.execute(new Runnable() {
@Override public void run() {
  ensureGone(reference, watchStartNanoTime);
}
  });
}


final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;
  public final String name;
}

可以看到,它首先把我們傳入的 activity 包裝成了一個KeyedWeakReference(可以暫時看成一個普通的 WeakReference),然後 watchExecutor 會去執行一個 Runnable,這個 Runnable 會呼叫 ensureGone(reference, watchStartNanoTime) 函式。

看這個函式之前猜測下,我們知道 watch 函式本身就是用來監聽 activity 是否被正常回收,這就涉及到兩個問題:
- 1.何時去檢查它是否回收?
- 2.如何有效地檢查它真的被回收?

所以我們覺得 ensureGone 函式本身要做的事正如它的名字,就是確保 reference 被回收掉了,否則就意味著記憶體洩漏。

核心函式:ensureGone(reference) 檢測回收

下面來看這個函式實現:

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime)     {
   removeWeaklyReachableReferences();
   if (gone(reference) || debuggerControl.isDebuggerAttached()) {
      return;
   }
   gcTrigger.runGc();
   removeWeaklyReachableReferences();
   if (!gone(reference)) {
      File heapDumpFile = heapDumper.dumpHeap();
      heapdumpListener.analyze(
      new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
           gcDurationMs, heapDumpDurationMs));
     }
  }

  private boolean gone(KeyedWeakReference reference) {
       return !retainedKeys.contains(reference.key);
  }

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

這裡先來解釋下WeakReferenceReferenceQueue 的工作原理。
1.弱引用 WeakReference
被強引用的物件就算髮生 OOM 也永遠不會被垃圾回收機回收;被弱引用的物件,只要被垃圾回收器發現就會立即被回收;被軟引用的物件,具備記憶體敏感性,只有記憶體不足時才會被回收,常用來做記憶體敏感快取器;虛引用則任意時刻都可能被回收,使用較少。
2.引用佇列 ReferenceQueue
我們常用一個 WeakReference<Activity> reference = new WeakReference(activity);,這裡我們建立了一個 reference 來弱引用到某個 activity,當這個 activity 被垃圾回收器回收後,這個 reference 會被放入內部的 ReferenceQueue 中。也就是說,從佇列 ReferenceQueue 取出來的所有 reference,它們指向的真實物件都已經成功被回收了。

然後再回到上面的程式碼。

在一個 activity 傳給 RefWatcher 時會建立一個唯一的 key 對應這個 activity,該 key 存入一個集合 retainedKeys 中。也就是說,所有我們想要觀測的 activity 對應的唯一 key 都會被放入 retainedKeys 集合中。

基於我們對 ReferenceQueue 的瞭解,只要把佇列中所有的 reference 取出來,並把對應 retainedKeys 裡的 key 移除,剩下的 key 對應的物件都沒有被回收。

  • 1.ensureGone 首先呼叫 removeWeaklyReachableReferences 把已被回收的物件的 key 從 retainedKeys 移除,剩下的 key 都是未被回收的物件;

    • 2.if (gone(reference)) 用來判斷某個 reference 的 key 是否仍在 retainedKeys 裡,若不在,表示已回收,否則繼續;

    • 3.gcTrigger.runGc(); 手動出發 GC,立即把所有 WeakReference 引用的物件回收;

    • 4.removeWeaklyReachableReferences(); 再次清理 retainedKeys,如果該 reference 還在 retainedKeys 裡 (if (!gone(reference))),表示洩漏;

    • 5.利用 heapDumper 把記憶體情況 dump 成檔案,並呼叫 heapdumpListener 進行記憶體分析,進一步確認是否發生記憶體洩漏。

    • 6.如果確認發生記憶體洩漏,呼叫 DisplayLeakService 傳送通知。

至此,核心的記憶體洩漏檢測機制便看完了。

記憶體洩漏檢測小結

從上面我們大概瞭解了記憶體洩漏檢測機制,大概是以下幾個步驟:
- 1.利用 application.registerActivityLifecycleCallbacks(lifecycleCallbacks) 來監聽整個生命週期內的 Activity onDestoryed事件;
- 2.當某個 Activity 被 destory 後,將它傳給 RefWatcher 去做觀測,確保其後續會被正常回收;
- 3.RefWatcher 首先把 Activity 使用 KeyedWeakReference 引用起來,並使用一個 ReferenceQueue來記錄該 KeyedWeakReference 指向的物件是否已被回收;
- 4.AndroidWatchExecutor 會在 5s 後,開始檢查這個弱引用內的 Activity 是否被正常回收。判斷條件是:若 Activity 被正常回收,那麼引用它的 KeyedWeakReference 會被自動放入 ReferenceQueue 中。
- 5.判斷方式是:先看 Activity 對應的 KeyedWeakReference 是否已經放入 ReferenceQueue 中;如果沒有,則手動 GC:gcTrigger.runGc();;然後再一次判斷 ReferenceQueue 是否已經含有對應的 KeyedWeakReference。若還未被回收,則認為可能發生記憶體洩漏。
- 6.利用 HeapAnalyzer 對 dump 的記憶體情況進行分析並進一步確認,若確定發生洩漏,則利用 DisplayLeakService 傳送通知。

探討一些關於 LeakCanary 有趣的問題

在學習了 LeakCanary 的原始碼之後,我想再提幾個有趣的問題做些探討。

LeakCanary 專案目錄結構為什麼這樣分?

下面是整個 LeakCanary 的專案結構:

對於開發者而言,只需要使用到 LeakCanary.install(this); 這一句即可。那整個專案為什麼要分成這麼多個 module 呢?

實際上,這裡面每一個 module 都有自己的角色。

  • leakcanary-watcher: 這是一個通用的記憶體檢測器,對外提供一個 RefWatcher#watch(Object watchedReference),可以看出,它不僅能夠檢測 Activity,還能監測任意常規的 Java Object 的洩漏情況。

  • leakcanary-android: 這個 module 是與 Android 世界的接入點,用來專門監測 Activity 的洩漏情況,內部使用了 application#registerActivityLifecycleCallbacks 方法來監聽 onDestory 事件,然後利用 leakcanary-watcher 來進行弱引用+手動 GC 機制進行監控。

-* leakcanary-analyzer:* 這個 module 提供了 HeapAnalyzer,用來對 dump 出來的記憶體進行分析並返回記憶體分析結果 AnalysisResult,內部包含了洩漏發生的路徑等資訊供開發者尋找定位。

  • leakcanary-android-no-op: 這個 module 是專門給 release 的版本用的,內部只提供了兩個完全空白的類 LeakCanary 和 RefWatcher,這兩個類不會做任何記憶體洩漏相關的分析。為什麼?因為 LeakCanary 本身會由於不斷 gc 影響到 app 本身的執行,而且主要用於開發階段的記憶體洩漏檢測。因此對於 release 則可以 disable 所有洩漏分析。

  • leakcanary-sample: 這個很簡單,就是提供了一個用法 sample。

當 Activity 被 destory 後,LeakCanary 多久後會去進行檢查其是否洩漏呢?

在原始碼中可以看到,LeakCanary 並不會在 destory 後立即去檢查,而是讓一個 AndroidWatchExecutor去進行檢查。它會做什麼呢?

 @Override public void execute(final Runnable command) {
  if (isOnMainThread()) {
      executeDelayedAfterIdleUnsafe(command);
  } else {
      mainHandler.post(new Runnable() {
  @Override public void run() {
    executeDelayedAfterIdleUnsafe(command);
  }
});
  }
}

void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
  // This needs to be called from the main thread.
   Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
  backgroundHandler.postDelayed(runnable, delayMillis);
    return false;
 }
  });
}

可以看到,它首先會向主執行緒的 MessageQueue 新增一個 IdleHandler。

什麼是 IdleHandler?我們知道 Looper會不斷從MessageQueue 裡取出 Message 並執行。當沒有新的Message 執行時,Looper 進入Idle 狀態時,就會取出 IdleHandler來執行。

換句話說,IdleHandler就是 優先級別較低的Message,只有當 Looper 沒有訊息要處理時才得到處理。而且,內部的 queueIdle() 方法若返回 true,表示該任務一直存活,每次 Looper 進入 Idle 時就執行;反正,如果返回 false,則表示只會執行一次,執行完後丟棄。

那麼,這件優先順序較低的任務是什麼呢?backgroundHandler.postDelayed(runnable, delayMillis);runnable 就是之前 ensureGone()。

也就是說,當主執行緒空閒了,沒事做了,開始向後臺執行緒傳送一個延時訊息,告訴後臺執行緒,5s(delayMillis)後開始檢查 Activity 是否被回收了。

所以,當 Activity 發生 destory 後,首先要等到主執行緒空閒,然後再延時 5s(delayMillis),才開始執行洩漏檢查。

知識點:

**1.如何建立一個優先順序低的主執行緒任務,它只會在主執行緒空閒時才執行,不會影響到 app 的效能?
**

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
   @Override public boolean queueIdle() {
     // do task
     return false; // only once
    }
 });

**
2.如何快速建立一個主/子執行緒 handler?**

// 主執行緒handler
mainHandler = new Handler(Looper.getMainLooper());

// 子執行緒handler
HandlerThread handlerThread = new HandlerThread(“子執行緒任務”);
handlerThread.start();
Handler backgroundHandler = new Handler(handlerThread.getLooper());

3.如何快速判斷當前是否執行在主執行緒?

 Looper.getMainLooper().getThread() == Thread.currentThread();

System.gc()可以觸發立即 gc 嗎?如果不行那怎麼才能觸發即時 gc 呢?

在 LeakCanary 裡,需要立即觸發 gc,並在之後立即判斷弱引用是否被回收。這意味著該 gc 必須能夠立即同步執行。

常用的觸發 gc 方法是 System.gc(),那它能達到我們的要求嗎?

我們來看下其實現方式:

/**
 * Indicates to the VM that it would be a good time to run the
 * garbage collector. Note that this is a hint only. There is no  guarantee
 * that the garbage collector will actually be run.
 */
public static void gc() {
   boolean shouldRunGC;
   synchronized(lock) {
      shouldRunGC = justRanFinalization;
       if (shouldRunGC) {
          justRanFinalization = false;
       } else {
         runGC = true;
       }
     }
     if (shouldRunGC) {
        Runtime.getRuntime().gc();
     }
  }

註釋裡清楚說了,System.gc()只是建議垃圾回收器來執行回收,但是 不能保證真的去回收。從程式碼也能看出,必須先判斷 shouldRunGC 才能決定是否真的要 gc。

知識點:

那要怎麼實現 即時 GC 呢?

LeakCanary 參考了一段 AOSP 的程式碼

 // System.gc() does not garbage collect every time. Runtime.gc() is
// more likely to perfom a gc.
  Runtime.getRuntime().gc();
  enqueueReferences();
  System.runFinalization();
  public static void enqueueReferences() {
    /*
    * Hack. We don't have a programmatic way to wait for the    reference queue
     * daemon to move references to the appropriate queues.
     */
    try {
         Thread.sleep(100);
     } catch (InterruptedException e) {
         throw new AssertionError();
     }
}

可以怎樣來改造 LeakCanary 呢?

忽略某些已知洩漏的類或 Activity

LeakCanary 提供了 ExcludedRefs 類,可以向裡面新增某些主動忽略的類。比如已知 Android 原始碼裡有某些記憶體洩漏,不屬於我們 App 的洩漏,那麼就可以 exclude 掉。

另外,如果不想監控某些特殊的 Activity,那麼可以在onActivityDestroyed(Activity activity) 裡,過濾掉特殊的 Activity,只對其它 Activity 呼叫 refWatcher.watch(activity) 監控。

把記憶體洩漏資料上傳至伺服器

在 LeakCanary 提供了
AbstractAnalysisResultService,它是一個 intentService,接收到的 intent 內包含了 HeapDump 資料和AnalysisResult 結果,我們只要繼承這個類,實現自己的 listenerServiceClass,就可以將堆資料和分析結果上傳到我們自己的伺服器上。

小結

本文通過原始碼分析了 LeakCanary 的原理,並提出了一些有趣的問題,學習了一些實用的知識點。希望對讀者有所啟發,歡迎與我討論。

之後會繼續挑選優質開源專案進行分析,歡迎提意見。

整理: