LeakCanary : 內存泄露檢測

什么是內存泄露

一些對象有著有限的生命周期。當這些對象所要做的事情完成了,我們希望他們會被回收掉。但是如果有一系列對這個對象的引用,那么在我們期待這個對象生命周期結束的時候被收回的時候,它是不會被回收的。它還會占用內存,這就造成了內存泄露。持續累加,內存很快被耗盡。

比如,當 Activity.onDestroy被調用之后,activity 以及它涉及到的 view 和相關的 bitmap 都應該被回收。但是,如果有一個后臺線程持有這個 activity 的引用,那么 activity 對應的內存就不能被回收。這最終將會導致內存耗盡,然后因為 OOM 而 crash。

LeakCanary

LeakCanarySquare公司開發的一個用于檢測OOM(out of memory的縮寫)問題的開源庫。你可以在 debug 包種輕松檢測內存泄露。
Github地址:https://github.com/square/leakcanary

如何使用

引入LeakCanary庫 ,在項目的build.gradle文件添加:

   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'

gradle 強大的可配置性,可以確保只在編譯 debug 版本時才會檢查內存泄露,而編譯 release 等版本的時候則會自動跳過檢查,避免影響性能。
在自定義的Application中初始化 ,LeakCanary.install()會返回一個預定義的 RefWatcher,同時也會啟用一個 ActivityRefWatcher,用于自動監控調用 Activity.onDestroy()之后泄的activity。

如果只想檢測Activity的內存泄露,只需要添加這一行代碼 。

 private static RefWatcher refWatcher;
 @Override
    public void onCreate() {
        super.onCreate();
        //LeakCanary 就會自動偵測 activity 的內存泄露
        // 會返回一個預定義的 RefWatcher ,同時也會啟用一個 ActivityRefWatcher,用于自動監控調用 Activity.onDestroy() 之后泄露的 activity。
        refWatcher = LeakCanary.install(this);
    }

如果還想檢測fragment

public class BaseFragment extends Fragment {
    //使用 RefWatcher 監控 Fragment:
    @Override
    public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = MyApplication.getRefWatcher();
        refWatcher.watch(this);
    }
}

當你在測試debug版本過程中出現內存泄露時,LeakCanary將會自動展示一個通知欄

screenshot.png

通過通知里的信息,我們可以解決很多內存泄露問題 。

備注:LeakCanary只支持4.0以上,原因是其中在watch 每個Activity時調用了Application的registerActivityLifecycleCallback函數,這個函數只在4.0上才支持,但是在4.0以下也是可以用的,可以在Application中將返回的RefWatcher存下來,然后在基類Activity的onDestroy函數中調用。

工作原理

簡單概述一下, 源碼還沒有分析明白 。

  1. RefWatcher.watch()創建一個 KeyedWeakReference 到要被監控的對象 (也就是弱引用)。
  2. 然后在后臺線程檢查引用是否被清除,如果沒有,調用GC。
  3. 如果引用還是未被清除,把 heap 內存 dump 到 APP 對應的文件系統中的一個 .hprof文件中。
  4. 在另外一個進程中的 HeapAnalyzerService有一個 HeapAnalyzer使用HAHA 解析這個文件。得益于唯一的 reference key, HeapAnalyzer找到 KeyedWeakReference,定位內存泄露。
  5. HeapAnalyzer計算 到 GC roots 的最短強引用路徑,并確定是否是泄露。如果是的話,建立導致泄露的引用鏈。
  6. 引用鏈傳遞到 APP 進程中的 DisplayLeakService, 并以通知的形式展示出來。

源碼層面簡單分析

RefWatch

ReftWatcher是leakcancay檢測內存泄露的發起點。使用方法為,在對象生命周期即將結束的時候,調用

    RefWatcher.watch(Object object)

為了達到檢測內存泄露的目的,RefWatcher需要

  private final Executor watchExecutor;
  private final DebuggerControl debuggerControl;
  private final GcTrigger gcTrigger;
  private final HeapDumper heapDumper;
  private final Set<String> retainedKeys;
  private final ReferenceQueue<Object> queue;
  private final HeapDump.Listener heapdumpListener;
  • watchExecutor: 執行內存泄露檢測的executor
  • debuggerControl :用于查詢是否正在調試中,調試中不會執行內存泄露檢測
  • gcTrigger: 用于在判斷內存泄露之前,再給一次GC的機會
  • headDumper: 用于在產生內存泄露室執行dump 內存heap
  • retainedKeys: 持有那些呆檢測以及產生內存泄露的引用的key
  • queue : 用于判斷弱引用所持有的對象是否已被GC。
  • heapdumpListener: 用于分析前面產生的dump文件,找到內存泄露的原因
    接下來,我們來看看watch函數背后是如何利用這些工具,生成內存泄露分析報告的。
 /**
   * Watches the provided references and checks if it can be GCed. This method is non blocking,
   * the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with.
   *
   * @param referenceName An logical identifier for the watched object.
   */
  public void watch(Object watchedReference, String referenceName) {
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    // 如果處于debug模式,直接return
    if (debuggerControl.isDebuggerAttached()) {
      return;
    }
    //記住開始觀測的時間
    final long watchStartNanoTime = System.nanoTime();
    //生成一個隨機的key,并加入set中
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    //生成一個KeyedWeakReference
    final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
     //調用watchExecutor,執行內存泄露的檢測
    watchExecutor.execute(new Runnable() {
      @Override public void run() {
           ensureGone(reference, watchStartNanoTime);
       }
    });
  }

所以最后的核心函數是在ensureGone這個方法里面。要理解其工作原理,就得從keyedWeakReference說起

WeakReference與ReferenceQueue

從watch函數中,可以看到,每次檢測對象內存是否泄露時,我們都會生成一個KeyedReferenceQueue,這個類其實就是一個WeakReference,只不過其額外附帶了一個key和一個name


/** @see {@link HeapDump#referenceKey}. */
final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;
  public final String name;

  KeyedWeakReference(Object referent, String key, String name,
      ReferenceQueue<Object> referenceQueue) {
      super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
      this.key = checkNotNull(key, "key");
      this.name = checkNotNull(name, "name");
  }
}

在構造時我們需要傳入一個ReferenceQueue,這個ReferenceQueue是直接傳入了WeakReference中,關于這個類,有興趣的可以直接看Reference的源碼。我們這里需要知道的是,每次WeakReference所指向的對象被GC后,這個弱引用都會被放入這個與之相關聯的ReferenceQueue隊列中。

在reference類加載的時候,java虛擬機會創建一個最大優先級的后臺線程,這個線程的工作原理就是不斷檢測pending是否為null,如果不為null,就將其放入ReferenceQueue中,pending不為null的情況就是,引用所指向的對象已被GC,變為不可達。

那么只要我們在構造弱引用的時候指定了ReferenceQueue,每當弱引用所指向的對象被內存回收的時候,我們就可以在queue中找到這個引用。如果我們期望一個對象被回收,那如果在接下來的預期時間之后,我們發現它依然沒有出現在ReferenceQueue中,那就可以判定它的內存泄露了。LeakCanary檢測內存泄露的核心原理就在這里。

監測時機

什么時候去檢測能判定內存泄露呢?這個可以看AndroidWatchExecutor的實現

public final class AndroidWatchExecutor implements Executor {
  private final Handler backgroundHandler;

  public AndroidWatchExecutor() {
    mainHandler = new Handler(Looper.getMainLooper());
    HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
    handlerThread.start();
    backgroundHandler = new Handler(handlerThread.getLooper());
  }
  ....
  private 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, DELAY_MILLIS);
         return false;
      }
    });
  }
}

這里又看到一個比較少的用法,IdleHandler,IdleHandler的原理就是在messageQueue因為空閑等待消息時給使用者一個hook。那AndroidWatchExecutor會在主線程空閑的時候,派發一個后臺任務,這個后臺任務會在DELAY_MILLIS時間之后執行。LeakCanary設置的是5秒。

二次確認保證內存泄露準確性

為了避免因為gc不及時帶來的誤判,leakcanay會進行二次確認進行保證。

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    //計算從調用watch到進行檢測的時間段
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //根據queue移除已被GC的對象的弱引用
    removeWeaklyReachableReferences();
    //如果內存已被回收或者處于debug模式,直接返回
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
      return;
    }
    //如果內存依舊沒被釋放,則再給一次gc的機會
    gcTrigger.runGc();
    //再次移除
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      //走到這里,認為內存確實泄露了
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();

      if (heapDumpFile == null) {
        // Could not dump the heap, abort.
        return;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, watchDurationMs, gcDurationMs,
              heapDumpDurationMs));
    }
  }

Dump Heap

監測到內存泄露后,首先做的就是dump出當前的heap,默認的AndroidHeapDumper調用的是

    Debug.dumpHprofData(filePath);

導出當前內存的hprof分析文件,一般我們在DeviceMonitor中也可以dump出hprof文件,然后將其從dalvik格式轉成標準jvm格式,然后使用MAT進行分析。

那么LeakCanary是如何分析內存泄露的呢?

HaHa

LeakCanary 分析內存泄露用到了一個和Mat類似的工具叫做HaHa,使用HaHa的方法如下:

 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 {
            HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
            HprofParser parser = new HprofParser(buffer);
            Snapshot snapshot = parser.parse();

            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);
        } catch (Throwable e) {
            return failure(e, since(analysisStartNanoTime));
        }
    }

返回的ActivityResult對象中包含了對象到GC root的最短路徑。LeakCanary在dump出hprof文件后,會啟動一個IntentService進行分析:HeapAnalyzerService在分析出結果之后會啟動DisplayLeakService用來發起Notification 以及將結果記錄下來寫在文件里面。以后每次啟動LeakAnalyzerActivity就從文件里讀取歷史結果。

參考文檔

LeakCanary 中文使用說明
LeakCanary 內存泄露監測原理研究 : 結合源碼分析leakcanary檢查內存泄露的過程。

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

推薦閱讀更多精彩內容