1. 程式人生 > >為什麼使用LeakCanary檢測記憶體洩漏?

為什麼使用LeakCanary檢測記憶體洩漏?

為什麼要使用LeakCanary?

不再需要的物件依然被引用,導致物件被分配的記憶體無法被回收,這就是記憶體洩漏的原因。

例如:一個Activity例項物件在呼叫了onDestory方法後是不再被需要的,如果儲存了一個引用Activity物件的靜態域,將導致Activity無法被垃圾回收器回收。

LeakCanary標識一個需要更長時間的物件,並找到防止其被垃圾收集的引用鏈。

引用鏈來自於垃圾回收器的**可達性分析演算法:當一個物件到GC Roots 沒有任何引用鏈相連時,則證明此物件時不可用的。**如圖: 在這裡插入圖片描述 物件object5、object6、object7 雖然互相有關聯,但是它們到 GC Roots 是不可達的,所以它們將會被判定為是可回收的物件。

在Java語言中,可作為GC Roots的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
  • 方法區中靜態屬性引用的物件。
  • 方法區中常量引用的物件。
  • 本地方法棧中JNI(即一般說的Native方法)引用的e物件。

LeakCanary是怎麼工作的?

  1. RefWatcher.watch() 建立了一個 KeyedWeakReference 來監視物件.
  2. 稍後,在後臺執行緒中,檢查引用是否已被清除,如果沒有,則觸發GC。
  3. 如果引用一直沒有被清除,它會dumps the heap 到一個.hprof 檔案中,然後將.hprof 檔案儲存到檔案系統。
  4. HeapAnalyzerService在單獨的程序中啟動,HeapAnalyzer使用
    HAHA
    來解析dump heap。
  5. HeapAnalyzer 通過唯一的引用key來找到heap dump 中的KeyedWeakReference,並定位記憶體洩漏引用。
  6. HeapAnalyzer計算到GC Roots的最短強引用鏈路徑來確定是否有洩漏,然後構建導致洩漏的引用鏈。
  7. 然後將結果返回到應用程式程序中的DisplayLeakService,並顯示洩漏通知。

原始碼分析

初始化

程式碼從LeakCanary.install(this)方法開始,它主要是建立一個RefWatcher物件,然後開始監視activity或fragment物件。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();
  }

  1. refWatcher(application):建立了一個AndroidRefWatcherBuilder類物件,AndroidRefWatcherBuilder類主要是設定一些預設資訊。
  2. listenerServiceClass(DisplayLeakService.class):設定監聽分析結果的監聽器。當記憶體洩漏結果分析完成,會呼叫DisplayLeakService監聽器的onHeapAnalyzed()方法。
  3. excludedRefs(AndroidExcludedRefs.createAppDefaults().build()):過濾掉一些由於SDK版本和製造廠商本身引起的記憶體洩漏問題。AndroidExcludedRefs是一個列舉類,它列舉了所遇到的記憶體洩漏問題。例如:在SDK版本19到21之間,ActivityClientRecord類中的nextIdle域變數會保持引用已經呼叫onDestroy()方法的Activity物件,導致記憶體洩漏。
  4. buildAndInstall():開始監視Activity和Fragment物件。

buildAndInstall()方法中的操作

在 buildAndInstall() 方法中:

 /**
 1. Creates a {@link RefWatcher} instance and makes it available through {@link
 2. LeakCanary#installedRefWatcher()}.
 3.  4. Also starts watching activity references if {@link #watchActivities(boolean)} was set to true.
 4.  6. @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();
    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.RefWatcher類

RefWatcher refWatcher = build(); 其實等於 RefWatcher refWatcher = new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,heapDumpBuilder);下面為RefWatcher的構造方法:

  RefWatcher(WatchExecutor watchExecutor, DebuggerControl debuggerControl, GcTrigger gcTrigger,
      HeapDumper heapDumper, HeapDump.Listener heapdumpListener, HeapDump.Builder heapDumpBuilder) {
    this.watchExecutor = checkNotNull(watchExecutor, "watchExecutor");
    this.debuggerControl = checkNotNull(debuggerControl, "debuggerControl");
    this.gcTrigger = checkNotNull(gcTrigger, "gcTrigger");
    this.heapDumper = checkNotNull(heapDumper, "heapDumper");
    this.heapdumpListener = checkNotNull(heapdumpListener, "heapdumpListener");
    this.heapDumpBuilder = heapDumpBuilder;
    retainedKeys = new CopyOnWriteArraySet<>();
    queue = new ReferenceQueue<>();
  }

  1. watchExecutor:WatchExecutor介面的實現類是AndroidWatchExecutor,AndroidWatchExecutor 類用於監視Android 物件洩漏,它等待主執行緒成變成空閒時,然後再延遲一個指定的時間傳送到後臺執行緒中去執行。
  2. debuggerControl:用於檢測當前是否正在除錯中,如果是則不會執行記憶體洩露檢測。
  3. gcTrigger:當一個被監視的物件預期弱可到達,但是尚未新增到引用佇列中時呼叫。這給應用程式提供了一個hook,以便在再次檢查引用佇列之前執行GC,以避免在可能的情況下進行 heap dump。
  4. heapDumper:用於dump heap,然後儲存到檔案中。
  5. heapdumpListener:用於分析dump heap後產生的檔案。
  6. heapDumpBuilder:dump heap需要的一些引數資訊。
  7. retainedKeys:用於儲存監視物件對應的key。
  8. queue:用於監聽被監視的物件是否已經被垃圾回收器回收。

2.DisplayLeakActivity元件

 LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);

  public static void setEnabledAsync(Context context, final Class<?> componentClass,
      final boolean enabled) {
    final Context appContext = context.getApplicationContext();
    AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
      @Override public void run() {
        setEnabledBlocking(appContext, componentClass, enabled);
      }
    });
  }

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

在子執行緒中去告訴系統不要關閉DisplayLeakActivity元件,DisplayLeakActivity其實就是我們見到的下面這個頁面:

3.開始監視Activity和Fragment物件

      if (watchActivities) {//watchActivities預設為true
        ActivityRefWatcher.install(context, refWatcher);//監視Activity物件
      }
      if (watchFragments) {//watchFragments預設為true
        FragmentRefWatcher.Helper.install(context, refWatcher);//監視Fragment物件
      }

監視Activity物件時,首先用Application物件註冊Activity的生命週期回撥監聽器,然後在Activity的onDestroy方法中呼叫:

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);//註冊Activity的生命週期回撥監聽器
  }

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          refWatcher.watch(activity);//監視 Activity物件
        }
      };

監視Fragment時,首先用Application物件註冊Activity的生命週期回撥監聽器。在Activity的onCreate方法呼叫時,用FragmentManager物件註冊Fragment的生命週期回撥監聽器。當Fragment的onDestroyView方法呼叫時,開始監視 Fragment的View物件;在Fragment的onDestroy方法呼叫時,開始監視 Fragment物件。

註冊Fragment的生命週期回撥監聽器:

 public static void install(Context context, RefWatcher refWatcher) {
      List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();

      if (SDK_INT >= O) {
        fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
      }

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

      Application application = (Application) context.getApplicationContext();
      application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }

    private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
        new ActivityLifecycleCallbacksAdapter() {
          @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            for (FragmentRefWatcher watcher : fragmentRefWatchers) {
              watcher.watchFragments(activity);
            }
          }
        };

這裡提供了SupportFragmentRefWatcher或AndroidOFragmentRefWatcher類來監聽Fragment的生命週期。SupportFragmentRefWatcher支援support包,AndroidOFragmentRefWatcher支援系統版本O以上。

下面為SupportFragmentRefWatcher類中註冊Fragment的生命週期回撥監聽器:

class SupportFragmentRefWatcher implements FragmentRefWatcher {
  private final RefWatcher refWatcher;

  SupportFragmentRefWatcher(RefWatcher refWatcher) {
    this.refWatcher = refWatcher;
  }

  private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {

        @Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
          View view = fragment.getView();
          if (view != null) {
            refWatcher.watch(view);//監視 View物件
          }
        }

        @Override public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
          refWatcher.watch(fragment);//監視 Fragment物件
        }
      };

  @Override public void watchFragments(Activity activity) {
    if (activity instanceof FragmentActivity) {
      FragmentManager supportFragmentManager =
          ((FragmentActivity) activity).getSupportFragmentManager();
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
    }
  }
}

在AndroidOFragmentRefWatcher也類似。

開始分析記憶體洩漏

在開始分析記憶體洩漏之前,先了解下弱引用(WeakReference)和引用佇列( ReferenceQueue):

  • 弱引用(WeakReference):弱引用物件,它們並不禁止其指示物件變得可終結,並被終結,然後被回收。弱引用最常用於實現規範化的對映。
  • 引用佇列( ReferenceQueue):引用佇列,在檢測到適當的可到達性更改後,垃圾回收器將已註冊的引用物件新增到該佇列中。

建立一個弱引用物件:

public class WeakReference<T> extends Reference<T> {

    //建立引用給定物件的新的弱引用。
    public WeakReference(T referent) {
        super(referent);
    }

  //建立引用給定物件的新的弱引用,並向給定佇列註冊該引用。
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

當Activity或者Fragment在呼叫onDestroy方法時,就會開始分析Activity或者Fragment物件是否會存在記憶體洩漏。

記憶體洩漏分析從RefWatcher類的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();//生成一個唯一的key,用於查詢KeyedWeakReference
    retainedKeys.add(key);//快取key
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);//包裝物件成弱引用,然後新增到ReferenceQueue中

    ensureGoneAsync(watchStartNanoTime, reference);//放到後臺執行緒中去執行
  }
  
  // 延遲指定的時間後傳送到後臺執行緒中去執行
    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();////如果物件沒有被垃圾回收,就手動觸發gc
    removeWeaklyReachableReferences();//再次移除弱可達的引用物件
    if (!gone(reference)) {//再次檢查物件是否從引用佇列中移除了,如果是就沒有記憶體洩漏,正常結束,否則繼續往下執行,開始dump heap
      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. RefWatcher → watch → ensureGoneAsync → (AndroidWatchExecutor → execute) → ensureGone →
  2. ServiceHeapDumpListener → analyze →
  3. HeapAnalyzerService → runAnalysis → onHandleIntentInForeground →
  4. AbstractAnalysisResultService → sendResultToListener → onHandleIntentInForeground → (DisplayLeakService) → onHeapAnalyzed → showNotification
  5. LeakCanaryInternals → showNotification → DisplayLeakActivity

第一步

首先將物件包裝成KeyedWeakReference,然後等待主執行緒空閒時,再延遲一個指定的時間傳送到後臺執行緒中執行。接下來是移除弱可達的物件:

 private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {//從佇列中移除引用物件
      retainedKeys.remove(ref.key);
    }
  }

引用佇列的poll()方法的意思是:輪詢此佇列,檢視是否存在可用的引用物件。如果存在一個立即可用的物件,則從該佇列中移除此物件並返回。否則此方法立即返回 null。

其次,再檢查當前監視的物件是否從引用佇列中移除了,如果是就代表沒有記憶體洩漏,停止執行;否則繼續向下執行,開始手動gc:

gcTrigger.runGc();

public void runGc() {
      Runtime.getRuntime().gc();
      enqueueReferences();
      System.runFinalization();
    }

當手動gc完成後,又重新移除弱可達的物件,檢查當前監視的物件是否從引用佇列中移除。如果物件還沒有移除,就開始dump heap進行分析。

第二步

在dump heap之前,還要先做一些準備工作,建立儲存檔案,以及要分析的記憶體洩漏物件的基本資訊。接下來呼叫heapdumpListener的analyze方法開始分析:

heapdumpListener.analyze(heapDump);

heapdumpListener就是在LeakCanary的install方法中listenerServiceClass(DisplayLeakService.class)賦的值:

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

接下呼叫ServiceHeapDumpListener類的analyze方法:

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

在analyze方法中又交給HeapAnalyzerService類的runAnalysis方法:

 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,所以此時執行在後臺服務中,接下來呼叫HeapAnalyzerService的是onHandleIntentInForeground方法:

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

在這個方法中,呼叫HeapAnalyzer的checkForLeak方法開始分析記憶體洩漏:

  /**
 1. Searches the heap dump for a {@link KeyedWeakReference} instance with the corresponding key,
 2. 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));
    }
  }

HeapAnalyzer就是最開始說的:

  1. HeapAnalyzer使用HAHA來解析dump heap。
  2. HeapAnalyzer 通過唯一的引用key來找到heap dump 中的KeyedWeakReference,並定位記憶體洩漏引用。
  3. HeapAnalyzer計算到GC Roots的最短強引用鏈路徑來確定是否有洩漏,然後構建導致洩漏的引用鏈。

分析完成之後,將分析結果封裝成AnalysisResult物件。最後交給AbstractAnalysisResultService的子類DisplayLeakService來處理結果,DisplayLeakService主要用來顯示通知,展示在使用者面前。就此,記憶體洩漏分析完成。

總結

LeakCanary用到的最主要的知識點就是垃圾回收器的可達性分析演算法:當一個物件到GC Roots 沒有任何引用鏈相連時,則證明此物件時不可用的。

要明白LeakCanary的記憶體洩漏檢測原理,需要了解掌握:

  1. Activity和Fragment的生命週期回撥
  2. 弱引用和引用佇列
  3. 垃圾回收演算法:可達性分析演算法

總之,LeakCanary是一個非常實用的檢測App中記憶體洩漏的工具,我們可以通過它來避免記憶體洩漏,讓應用程式更加的完美。