原始碼分析二:LeakCanary

楊昆發表於2018-03-02

分析記憶體洩漏分析工具

核心一是ReferenceQueue,這是一個用來儲存Reference的佇列,具體見WeakReference的構造方法:

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T var1) {
        super(var1);
    }

    public WeakReference(T var1, ReferenceQueue<? super T> var2) {
        super(var1, var2);
    }
}複製程式碼

先了解WeakReference和ReferenceQueue:

WeakReference和ReferenceQueue

首先需要知道WeakReference。當GC執行緒掃描它所管轄的記憶體區域時,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否(這一點與SoftReference不同),都會回收它的記憶體。由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。

                                                WeakReference_obj

WeakReference和ReferenceQueue聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。上圖中的實線a表示強引用,虛線aa表示弱引用。如果切斷a,那麼Object物件將會被回收。正如下面這段測試程式碼所示。

A a = new A();
 ReferenceQueue queue = new ReferenceQueue();
 WeakReference aa = new WeakReference(a, queue);
 a = null;
 Runtime.getRuntime().gc();
 System.runFinalization();
 try {
 
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 Reference poll = null;
 while ((poll = queue.poll()) != null) {
 System.out.println(poll.toString());
 }
複製程式碼

當垃圾回收器回收物件的時候,aa這個弱引用將會入隊進入ReferenceQueue,所以queue.poll()將不會為空,除非這個物件沒有被垃圾回收器清理。ok了,開始分析程式。

程式入口

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

關鍵在buildAndInstall這個方法

public RefWatcher buildAndInstall() {
  RefWatcher refWatcher = build();
  if (refWatcher != DISABLED) {
    LeakCanary.enableDisplayLeakActivity(context);
    ActivityRefWatcher.install((Application) context, refWatcher);//關鍵程式碼
  }
  return refWatcher;
}複製程式碼

看install

public static void install(Application application, RefWatcher refWatcher) {
  new ActivityRefWatcher(application, refWatcher).watchActivities();
}複製程式碼

watchActivities

public void watchActivities() {
  // Make sure you don't get installed twice.
  stopWatchingActivities();
  application.registerActivityLifecycleCallbacks(lifecycleCallbacks);//4.0提供的生命週期的監測
}複製程式碼

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);//在Activity Destroy的時候去監測記憶體洩漏

      }
    };複製程式碼

onActivityDestroyed:

void onActivityDestroyed(Activity activity) {
  refWatcher.watch(activity);
}複製程式碼

到這裡,可以看到leakcanary在4.0以上系統的監測是通過ActivityLifecycleCallbacks來監測的,監測時機是在onActivityDestroyed。具體監測方法是在refWatcher,繼續往下看。

refWatcher#watch

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
  retainedKeys.add(key);//retainedKeys中新增這個key
  final KeyedWeakReference reference =
      new KeyedWeakReference(watchedReference, key, referenceName, queue);
    //包裝成WeakReference並新增到ReferenceQueue
  ensureGoneAsync(watchStartNanoTime, reference);
}複製程式碼

主要就是封裝。且用retainedKeys這個set來儲存每個引用的標識key。後面將會用到。封裝完成就要開始分析了,核心方法是ensureGone:

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)) {//好,回收了,那麼這個activity沒有洩漏
    return DONE;
  }
  gcTrigger.runGc();//還是沒有回收,手動GC一下
  removeWeaklyReachableReferences();//再看看物件回收沒有
  if (!gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

    File heapDumpFile = heapDumper.dumpHeap();
//竟然還沒回收,那麼懷疑是記憶體洩漏了,所以下一步dump記憶體快照.hprof下來進一步精確分析
    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;
}複製程式碼

來看removeWeaklyReachableReferences這個方法,是如何確定是否回收。

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);
  }
}複製程式碼

1、如果這個物件作為弱引用,被回收了,那麼java虛擬機器會自動將他新增到引用佇列(ReferenceQueue)當中。

2、這個時候去queue.poll,將拿到弱引用ref(Activity)對應的key,retainedKeys去刪除掉這個key(之前有往裡面新增)。表明該物件的引用已經釋放。

3、再用gone(reference)來確定retainedKeys是否已經刪除對應的key了。

4、如果沒有刪除,則手動再gc一次(之前都是系統回收)。

5、再確認一下,如果還沒有回收(即retainedKeys中還有這個key),則dump下來記憶體(.hprof檔案)進行分析。

.hprof記憶體分析

通過上面的二次確認,可以確定某個object(Activity或者fragment)存在記憶體洩漏。那麼接下來就是dump下來記憶體並進行analyze。

 File heapDumpFile = heapDumper.dumpHeap();
//竟然還沒回收,那麼懷疑是記憶體洩漏了,所以下一步dump記憶體快照.hprof下來進一步精確分析
    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));複製程式碼

呼叫ServiceHeapDumpListener#analyze

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

最終呼叫到HeapAnalyzerService#onHandleIntent,其中HeapAnalyzerService是獨立執行在:leakcanary程式:

@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);
  //這裡分析heapDump
  AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  //這裡將分析結果傳送出去做展示
}複製程式碼

著重在HeapAnalyzer#checkForLeak

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); 
    //1.把.hprof轉為Snapshot,這個Snapshot物件就包含了物件引用的所有路徑
    HprofParser parser = new HprofParser(buffer);
    //2.精簡gcroots??
    Snapshot snapshot = parser.parse();
    deduplicateGcRoots(snapshot);

    Instance leakingRef = findLeakingReference(referenceKey, snapshot);
    //3.找出洩漏的物件
    
    if (leakingRef == null) {
      return noLeak(since(analysisStartNanoTime));
    }

    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    //4.找出洩漏物件的最短路徑
  } catch (Throwable e) {
    return failure(e, since(analysisStartNanoTime));
  }
}複製程式碼

上面的checkForLeak方法就是輸入.hprof,輸出分析結果,主要有以下幾個步驟:

1.把.hprof轉為Snapshot,這個Snapshot物件就包含了物件引用的所有路徑

2.精簡gcroots,把重複的路徑刪除,重新封裝成不重複的路徑的容器

3.找出洩漏的物件

4.找出洩漏物件的最短路徑

重點分析放在第3、4步:(很多處理邏輯都是haha這個第三方庫實現的)

private Instance findLeakingReference(String key, Snapshot snapshot) {
  ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
  List<String> keysFound = new ArrayList<>();
  for (Instance instance : refClass.getInstancesList()) {
    List<ClassInstance.FieldValue> values = classInstanceValues(instance);
    String keyCandidate = asString(fieldValue(values, "key"));
    if (keyCandidate.equals(key)) {
      return fieldValue(values, "referent");
    }
    keysFound.add(keyCandidate);
  }
  throw new IllegalStateException(
      "Could not find weak reference with key " + key + " in " + keysFound);
}複製程式碼

那麼上面這個方法是在snapshot快照中找到第一個弱引用(因為就是這個物件沒有回收,洩漏了),然後根據遍歷這個物件的所有例項,如果key值和最開始定義封裝的key值相同,那麼返回這個洩漏物件,就是已近在快照中定位到了洩漏物件了。

繼續看findLeakTrace

private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
    Instance leakingRef) {

  ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
  ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

  // False alarm, no strong reference path to GC Roots.
  if (result.leakingNode == null) {
    return noLeak(since(analysisStartNanoTime));
  }

  LeakTrace leakTrace = buildLeakTrace(result.leakingNode);

  String className = leakingRef.getClassObj().getClassName();

  // Side effect: computes retained size.
  snapshot.computeDominators();

  Instance leakingInstance = result.leakingNode.instance;

  long retainedSize = leakingInstance.getTotalRetainedSize();

  // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
  if (SDK_INT <= N_MR1) {
    retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
  }

  return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
      since(analysisStartNanoTime));
}複製程式碼

最後一個步驟是根據上一部得到的洩漏物件找到最短路徑,封裝在ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

總結:在第三個分析步驟,解析hprof檔案中,是先把這個檔案封裝成snapshot,然後根據弱引用和前面定義的key值,確定洩漏的物件,最後找到最短洩漏路徑,作為結果反饋出來,那麼如果在快照中找不到這個懷疑洩漏的物件,那麼就認為這個物件其實並沒有洩漏,因為已經回收了,如下的程式碼:

if (leakingRef == null) {
  return noLeak(since(analysisStartNanoTime));
}複製程式碼

總結、RefWatcher工作流程:

  • RefWatcher.watch() 建立一個 KeyedWeakReference 到要被監控的物件。
  • 然後在後臺執行緒檢查引用是否被清除,如果沒有,呼叫GC。
  • 如果引用還是未被清除,把 heap 記憶體 dump 到 檔案系統中的一個 .hprof 檔案中。
  • 在另外一個程式中,HeapAnalyzerService 通過 HeapAnalyzer 使用HAHA 解析這個檔案。
  • 得益於唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位記憶體洩漏。
  • HeapAnalyzer 計算 到 GC roots 的最短強引用路徑,並確定是否洩漏。如果是,建立導致洩漏的引用鏈。
  • 引用鏈傳遞到 APP 程式中的 DisplayLeakService, 並以通知的形式展示出來。

      原始碼分析二:LeakCanary

以上,就是leakCanary的全部流程。


相關文章