性能優化(2.1)-LeakCanary原理分析

主目錄見:Android高級進階知識(這是總目錄索引)
?性能優化很重要的一個環節就是檢測有沒有內存泄漏,以前我們內存泄漏會借助MAT,androidstudio Monitor(androidstudio 3.0改成Android profiler)等工具,檢測過程會比較麻煩一點,而LeakCanary作為一個自動內存泄漏工具出現,應該說它的簡單易用給我們省了好多工作量,提升了我們的代碼質量。也許大家會說,java不是有自動垃圾回收機制嗎?但是其實一些持有外部引用超過他應有的生命周期的話,那么這個時候垃圾回收機制是不會去回收的,這時候就會出現不可預期地內存暴走。

一.目標

今天的目標就是為了能明白LeakCanary大致的原理過程,然后大家能更放心使用,具體目標如下:
1.明白LeakCanary版本的差異;
2.LeakCanary的內存檢測思路。

二.源碼分析

具體的使用我們就不在這邊說了,因為github上面都有,而且現在4.0版本以上的使用變得簡單很多,我們來在代碼中的使用:

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);
    // Normal app init code...
  }
}

很簡單,但是其實在4.0以上其實才變得如此易用,這是為什么呢?這跟Application.ActivityLifecycleCallbacks這個方法有關(這個接口在Android 4.0才有),這個方法其實我們在前面的換膚框架實現解析(二)這篇文章有講解過,這個方法可以監測到Activity的各個生命周期,然后在LeakCanary就可以在onActivityDestroyed方法中為所有的Activity調用refWatcher.watch(activity)

總體流程

在分析LeakCanary之前,我們先來明確一下總體流程,檢測主要分為三個步驟:

  • 1.分析是否有可疑的泄漏對象,主要是通過弱引用機制來檢測;
  • 2.如果第一步發現了可疑泄漏對象,那么就會dump內存快照,然后分析.hprof文件確定是否真的泄漏了。
  • 3.展示分析的結果。

1.分析可疑泄漏對象

我們知道,因為我們應用了Application.ActivityLifecycleCallbacks(Activity)方法,所以我們程序會在ActivityRefWatcher類中注冊一個lifecycleCallbacks對象來監測Activity的生命周期,LeakCanary就是在onActivityDestroyed里面調用了refWatcher.watch(activity)方法,這里的refWatcher是在前面build()方法中初始化的,我們現在直接看RefWatcher的watch()方法:

 public void watch(Object watchedReference) {
    watch(watchedReference, "");
  }

這里的watchedReference就是我們的每個activity對象,我們看到這個方法又調用了內部兩個參數的watch方法:

 public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
//對一個引用產生一個唯一的Key
    String key = UUID.randomUUID().toString();
//放到key集合中
    retainedKeys.add(key);
//將要監測的對象添加一個弱引用
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
//在子線程中分析這個弱引用
    ensureGoneAsync(watchStartNanoTime, reference);
  }

從上面的代碼可以知道,其實就是給監測對象添加一個弱引用,然后使用ReferenceQueue來監測它的可達性的改變,其中key是一個唯一的uuid,而最后的ensureGoneAsync()是我們主要的分析方法了,我們來看看:

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

我們看到程序中watchExecutor是個什么東西呢?LeakCanary為我們實現了AndroidWatchExecutor,這里面利用HandlerThread的機制,在子線程中來處理分析這個邏輯(如果這個地方不懂,推薦看HandlerThread源碼分析),我們主要的分析方法是在ensureGone中,我們直接來看:

 Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//刪除所有已經在ReferenceQueue中的弱引用
    removeWeaklyReachableReferences();
//如果當前處于調試狀態則返回
    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
//如果當前的對象只有弱引用了,那么說明不會泄露
    if (gone(reference)) {
      return DONE;
    }
//如果當前的對象還沒有改變弱可達狀態,則我們手動調用GC
    gcTrigger.runGc();
//再次刪除,確認對象是不是已經在ReferenceQueue中
    removeWeaklyReachableReferences();
//如果當前對象還沒有在ReferenceQueue,說明可能泄露了,則dump內存快照
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
//我們開始dump內存快照
      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }

這里我們看到,我們為啥最后還有dump內存快照,然后進行分析.hprof文件呢,其實我們這里的GC只是建議虛擬機說進行一次內存回收,但是最終要不要進行內存回收是JVM說了算,如果這里建議沒被通過的時候,那么我們的可達性就不會發生改變,我們就需要第二個步驟dump內存快照來分析。

2.dump內存快照

我們看到程序的最后調用了heapdumpListeneranalyze方法,那么這里的heapdumpListener是什么呢?這里要從LeakCanary類中的install()方法看起:

  public static RefWatcher install(Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }

我們這里有個方法listenerServiceClass,這個方法我們跟進去看下:

  public AndroidRefWatcherBuilder listenerServiceClass(
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
  }

從這里我們可以看到我們的heapdumpListener其實就是我們的ServiceHeapDumpListener類對象,所以我們看到這個類的analyze方法:

 @Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }

HeapAnalyzerService是個IntentService的子類(同樣的,不懂IntentService的話推薦IntentService源碼分析),所以我們的主要方法是在onHandIntent方法中分析的:

 @Override protected void onHandleIntent(Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);

    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }

我們看到程序new了一個HeapAnalyzer對象,這個類主要負責分析hprof文件的。然后程序會調用HeapAnalyzercheckForLeak方法:

  public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return failure(exception, since(analysisStartNanoTime));
    }

    try {
//加載hprof文件
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
//解析
      Snapshot snapshot = parser.parse();
//精簡gcroots,把重復的路徑刪除,重新封裝成不重復的路徑的容器
      deduplicateGcRoots(snapshot);
//找到泄漏對象的引用
      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));
      }
 //查找從這個對象的引用到GC ROOT的最短路徑
      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

上面的代碼邏輯應該算是比較簡單,具體細節大家也不需要硬摳,我們知道,我們上面的代碼主要就是為了尋找到hprof文件中泄漏對象的引用路徑(泄漏對象到gcroot的最短路徑),如果能找到說明我們的對象確實泄漏了,最后會調用AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result)將發送出去。

3.泄漏結果展示

泄漏結果主要是在DisplayLeakService類中實現的,實現方法也不是很麻煩,大家可以自行查看,以為不在于主流程中,我們暫時就不講了。

總結:我們知道我們android系統中可能自身存在一些泄漏情況,所以我們LeakCanary提供了AndroidExcludedRefs類來進行排除監測,這樣我們不需要在乎Framework層本身的泄漏問題。現在LeakCanary的使用越來越多了,希望我們也能適當在代碼中引入來檢測自己寫的代碼是否有泄漏的風險,進而提升我們的代碼質量,當然我們平時也要關注一些常見的內存泄漏情況,我們可以參考MAT內存泄漏分析(一)MAT內存泄漏分析(二),最后祝大家性能優化之路愉快。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容