1. 程式人生 > >LeakCanary詳解原理分析

LeakCanary詳解原理分析

概述

上一篇LeakCanary使用詳細教程中,我們熟悉了LeakCanary的使用和初步描述了它的工作機制,這篇我準備從原始碼的角度去分析LeakCanary的工作原理;

原始碼分析

從上一篇中我們知道,LeakCanary在Appaction的初始化方式:

@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已經可以正常監控應用中所有Activity的記憶體洩漏情況了,那它是怎麼做到的呢?我們帶著這個問題來看一下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 refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }
  
  public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
      return new AndroidRefWatcherBuilder(context);
  }

要點分析:

1、install是有返回值的—RefWatcher,這個物件非常重要,LeakCanary就是通過這個物件進行Activity的記憶體監控
2、RefWatcher是通過Builder模式建立的;
3、install內部一連串的鏈式呼叫,最後呼叫了buildAndInstall()方法;
  private boolean watchActivities = true;
  private boolean watchFragments = true;

  /**
   * 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() {
    if (LeakCanaryInternals.installedRefWatcher != null) {
      throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }
    RefWatcher refWatcher = build();//RefWatch就是在這邊建立的
    if (refWatcher != DISABLED) {
      LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
      if (watchActivities) {
        ActivityRefWatcher.install(context, refWatcher);
      }
      if (watchFragments) {
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
  }

要點分析:

1、LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);允許洩漏報告頁面的顯示;
2、watchActivities/watchFragments預設值都是true,所以ActivityRefWatcher和FragmentRefWatcher都將被啟用;
3、ActivityRefWatcher用於監控Activity的記憶體洩漏;
4、FragmentRefWatcher用於監控Fragment的記憶體洩漏;

我們就用ActivityRefWatcher來進一步分析LeakCanary是如何監控記憶體洩漏的:

ActivityRefWatcher.install(context, refWatcher)方法:

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

要點分析:

1、registerActivityLifecycleCallbacks是Android Application的一個方法,註冊了該方法,應用中每一個Activity的生命週期變化都會通過該方法回調回來;
2、registerActivityLifecycleCallbacks方法傳入的引數是activityRefWatcher.lifecycleCallbacks,我們到ActivityRefWatcher中看下該方法的實現:
    private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          refWatcher.watch(activity);
        }
      };

從上面的實現來看,ActivityRefWatcher只監聽了onActivityDestroyed方法,也就是說每一個Activity呼叫onDestory的時候都會回撥到onActivityDestroyed這個方法,通知LeakCanary 該Activity應該被銷燬;

到這來我們知道了為什麼只執行LeakCanary.install(this)一條語句就可以完成對整個app的Activity記憶體洩漏進行監聽了;

接下來我們來看看ActivityRefWatcher是如何監聽記憶體洩漏的,RefWatcher有兩個核心方法: watch() 和 ensureGone()

watch方法:
/**
   * Watches the provided references and checks if it can be GCed. This method is non blocking,
   * the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed
   * with.
   *
   * @param referenceName An logical identifier for the watched object.
   */
  public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }

要點分析:

1、首先我們來翻譯一下,官方對watch方法的註釋:

監聽提供的引用,檢查該引用是否可以被回收。這個方法是非阻塞的,因為檢測功能是在Executor中的非同步執行緒執行的;

2、checkNotNull:分別檢測watchedReference、referenceName是否為空,如果為空則丟擲異常結束;
3、隨機生成一個key,該key用於唯一標識已產生記憶體洩漏的物件,或者準備檢測的物件;
4、建立KeyedWeakReference物件,並呼叫另一個核心方法ensureGone;
  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }


@SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {
      return DONE;
    }
    gcTrigger.runGc();
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }

要點分析:

1、ensureGone方法是在ensureGoneAsync方法中開啟的,並且是一個非同步任務,不會對主執行緒造成阻塞;
2、removeWeaklyReachableReferences():移除已經回收的弱引用物件的key,如果需要判斷的物件已經銷燬,則不繼續執行
if (gone(reference)) {
      return DONE;
    }
3、如果移除之後還是存在該引用則手動再GC一次:gcTrigger.runGc();
4、然後二次呼叫removeWeaklyReachableReferences()再判斷是否已經銷燬;
5、如果二次確認還是存在則判斷為記憶體洩漏了開始.hprof檔案的dump;
6、呼叫heapdumpListener.analyze(heapDump)進行洩漏分析;

階段總結一下:

1. 弱引用與ReferenceQueue聯合使用,如果弱引用關聯的物件被回收,則會把這個弱引用加入到ReferenceQueue中;通過這個原理,可以看出removeWeaklyReachableReferences()執行後,會對應刪除KeyedWeakReference的資料。如果這個引用繼續存在,那麼就說明沒有被回收。
2. 為了確保最大保險的判定是否被回收,一共執行了兩次回收判定,包括一次手動GC後的回收判定。兩次都沒有被回收,很大程度上說明了這個物件的記憶體被洩漏了,但並不能100%保證;因此LeakCanary是存在極小程度的誤差的。

hprof檔案已經生成好了,接下來就是進行記憶體洩漏最短路徑分析了:

上一步heapdumpListener.analyze(heapDump)的呼叫即為分析入口:

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

要點分析:

1、呼叫了HeapAnalyzerService,在單獨的程序中進行分析;

我們直接把HeapAnalyzerService整個類的程式碼貼上去:

public final class HeapAnalyzerService extends ForegroundService
    implements AnalyzerProgressListener {

  private static final String LISTENER_CLASS_EXTRA = "listener_class_extra";
  private static final String HEAPDUMP_EXTRA = "heapdump_extra";

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

  public HeapAnalyzerService() {
    super(HeapAnalyzerService.class.getSimpleName(), R.string.leak_canary_notification_analysing);
  }

  @Override protected void onHandleIntentInForeground(@Nullable 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, this, heapDump.reachabilityInspectorClasses);

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

  @Override public void onProgressUpdate(Step step) {
    int percent = (int) ((100f * step.ordinal()) / Step.values().length);
    CanaryLog.d("Analysis in progress, working on: %s", step.name());
    String lowercase = step.name().replace("_", " ").toLowerCase();
    String message = lowercase.substring(0, 1).toUpperCase() + lowercase.substring(1);
    showForegroundNotification(100, percent, false, message);
  }
}

要點分析:

1、runAnalysis方法中最後一句:ContextCompat.startForegroundService(context, intent)啟動HeapAnalyzerService
2、HeapAnalyzerService繼承自ForegroundService,服務啟動時會呼叫onHandleIntentInForeground方法;

onHandleIntentInForeground方法中比較核心的方法呼叫是:

    HeapAnalyzer heapAnalyzer =
            new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
    
    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
            heapDump.computeRetainedHeapSize);

HeapAnalyzer中核心的方法是checkForLeak,我們來看下它的具體實現:

  /**
   * 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) {
    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);
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
      listener.onProgressUpdate(PARSING_HEAP_DUMP);
      Snapshot snapshot = parser.parse();
      listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
      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));
    }
  }

要點分析:

1、先來翻譯一下官方對該方法的註釋:

在dump的堆資訊中通過key查找出記憶體洩漏的弱引用物件,並且計算出最短的GC路徑;

2、Snapshot snapshot = parser.parse()–生成檔案的快照
3、deduplicateGcRoots(snapshot)–過濾重複的記憶體洩漏物件;
4、findLeakingReference(referenceKey, snapshot)–在快照中根據referenceKey查詢是否有對於的記憶體洩漏物件,如果獲取到的leakingRef為空,則說明記憶體已經被回收了,不存在記憶體洩漏的情況;如果leakingRef不為空則進入下一步查詢記憶體洩漏物件的GC最短路徑;
5、findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize)–查詢記憶體洩漏物件的GC最短路徑;

好了LeakCanary的核心原始碼已經都已經分析完了,剩下的就是如果在頁面上顯示出來了,這部分就不在這邊討論了,有興趣的同學可以下載LeakCanary的原始碼看一下;

回顧一下我們剛剛分析過的重要的類和方法

方法 備註
LeakCanary install() SDK初始化介面,通常我們只需要呼叫該方法就可以監控app的記憶體洩漏情況;
DisplayLeakActivity 記憶體洩漏的檢視顯示頁面
RefWatcher watch() ensureGone() watch()–監聽提供的引用,檢查該引用是否可以被回收。這個方法是非阻塞的,因為檢測功能是在Executor中的非同步執行緒執行的;ensureGone()–2次確認是否確實存在記憶體洩漏,如果存在記憶體洩漏則,dump洩漏資訊到hprof檔案中,並呼叫ServiceHeapDumpListener回撥進行記憶體分析;
ActivityRefWatcher Activity引用檢測, 包含了Activity生命週期的監聽執行與停止
HeapAnalyzerService runAnalysis 記憶體堆分析服務, 為了保證App程序不會因此受影響變慢&記憶體溢位,運行於獨立的程序
HeapAnalyzer checkForLeak 在dump的堆資訊中通過key查找出記憶體洩漏的弱引用物件,並且計算出最短的GC路徑

總結

LeakCanary主要是通過Application的registerActivityLifecycleCallbacks方法監控每一個Activty的Destory之後物件是否被收回

1、在Activity Destory之後將該Activty放入一個弱引用WeakReference中;

2、將這個弱引用關聯到一個ReferenceQueue;

當一個軟引用/弱引用物件被垃圾回收後,Java虛擬機器就會把這個引用加入到與之關聯的引用佇列中;

3、檢視ReferenceQueue是否存在該Activtiy的引用;

4、如果該Activity洩漏了,LeakCanary工具將會dump出heap資訊,然後會分析出記憶體洩漏的路徑;