帶你學開源項目:LeakCanary-如何檢測 Activity 是否泄漏

版權聲明:本文原創發佈於公衆號 wingjay,轉載請務必註明出處! https://www.jianshu.com/p/87f2ba180066

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);
        }
      };

原來它只監聽了所有 ActivityonActivityDestroyed 事件,當 ActivityDestory 時,調用 ActivityRefWatcher.this.onActivityDestroyed(activity); 函數。

猜測下,正常情況下,當一個這個函數應該 activityDestory 時,那這個 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 的項目結構:

281665-4769dc1acf9581a0.png

對於開發者而言,只需要使用到 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 的版本用的,內部只提供了兩個完全空白的類 LeakCanaryRefWatcher,這兩個類不會做任何內存泄漏相關的分析。爲什麼?因爲 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
      }
    });
  1. 如何快速創建一個主/子線程handler?
//主線程handler
mainHandler = new Handler(Looper.getMainLooper());
//子線程handler
HandlerThread handlerThread = new HandlerThread(“子線程任務”);
handlerThread.start();
Handler backgroundHandler = new Handler(handlerThread.getLooper());
  1. 如何快速判斷當前是否運行在主線程?
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 的原理,並提出了一些有趣的問題,學習了一些實用的知識點。希望對讀者有所啓發,歡迎與我討論。

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

謝謝。

wingjay

281665-9ffa921d5b9d214a.jpg
Android技術·面試技巧·職業感悟
發佈了45 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章