alighters

程序、写作、人生

LeakCanary 浅析

| Comments

内存分析工具

关于内存分析,在 LeakCanary 之前,可以用到的工具主要以 MAT 为主,在新版的 AS 3.0 中,又提供了 Memory Profiler,可进一步帮助我们定位内存出现的问题。

Memory Profiler Android Profiler in Android Studio 3.0

利用Android Studio、MAT对Android进行内存泄漏检测 - 悦跑圈技术团队的博客 \| Joyrun’s Blog

View the Java Heap and Memory Allocations with Memory Profiler \| Android Studio

原理描述

其监听了 Application 中的 activityLifeCallback,在此会对得到的 activity 添加弱引用的方式,根据其特点来判断是否发生泄露。当泄露时,则 dump 出一份内存快照,得到 hprof 数据,再利用 square 的 haha 库,在另外的进程中,进行解析并显示。

主要流程

1.类的定位

在 Android 的 application onCreate 时,我们一般会调用 LeakCanary 的 install 方法,其所作的工作:

  • 1.通过 application 生成一个 AndroidRefWatcherBuilder
  • 2.注册监听的 service 为 DisplayLeakService
  • 3.设置排除的引用 AndroidExcludedRefs
  • 4.调用 AndroidRefWatcherBuilder 的 buildAndInstall 方法,其会调用到 ActivityRefWatcher 的 install 方法:
1
2
3
  public static void install(Application application, RefWatcher refWatcher) {
    new ActivityRefWatcher(application, refWatcher).watchActivities();
  }

其会构造出 ActivityRefWatcher,然后调用其 watchActivities 方法:

1
2
3
4
5
  public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }

这里主要的工作便是给 application 注册 ActivityLifecycleCallbacks,此类主要重写了 Application.ActivityLifecycleCallbacks 的 onActivityDestoryed 方法:

1
2
3
@Override public void onActivityDestroyed(Activity activity) {
 ActivityRefWatcher.this.onActivityDestroyed(activity);
}

在它的 onActivityDestoryed 方法中,其主要调用 RefWatcher 类的 watch 方法,其对象便是传递进来的 activity。

2.类的监测

接下来便是关键的 RefWatcher 类了,在其 watch 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

这里会将 watchedReference、referenceName、queue 包装成一个 KeyedWeakReference 对象。 其中 queue 是一个成员变量 ReferenceQueue 的对象。而 KeyedWeakReference 是一个继承自 WeakReference 的类。

关于 WeakReference 的官方描述:

Weak reference objects, which do not prevent their referents from being made finalizable, finalized, and then reclaimed. Weak references are most often used to implement canonicalizing mappings.

Suppose that the garbage collector determines at a certain point in time that an object is weakly reachable. At that time it will atomically clear all weak references to that object and all weak references to any other weakly-reachable objects from which that object is reachable through a chain of strong and soft references. At the same time it will declare all of the formerly weakly-reachable objects to be finalizable. At the same time or at some later time it will enqueue those newly-cleared weak references that are registered with reference queues.

简单来说就是弱引用并不会影响对象的回收。当一个对象被回收之时,之前对此对象的持有弱引用的对象会被标记为 finalizable,与此同时或之后会将其添加至之前注册的 reference queue 中。

这样的话,通过弱引用持有 activity,在 application 注册的 activitylifecallback 中,当 activity 退出时,在 referencequeue 里查询不在其中的 activity,此时,便确定了泄露的类。接下来便是 dump 出内存,来分析出泄露的路径了。

3.类的泄露定位

在 ensureGoneAsync 方法中,会执行如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);
   heapdumpListener.analyze(
       new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
           gcDurationMs, heapDumpDurationMs));
 }
 return DONE;
}

其中调用的 removeWeaklyReachableReferences 方法,则是对 refernceQueue 的排查:

1
2
3
4
5
6
7
8
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);
 }
}

这里从 queue 中,取出 KeyedWeakReference,并从 retainedKeys 集合中移除此 reference 对应的 key 值。

而判断此 reference 是否移除则是通过 gone 方法判断:

1
2
3
private boolean gone(KeyedWeakReference reference) {
 return !retainedKeys.contains(reference.key);
}

当集合中不包含此 WeakReference 对应的 key,则表示此弱引用持有的对象已被回收;否则表示有可能内存泄露。

这里进行了再一步的判断,触发调用一下 gc 方法:

1
gcTrigger.runGc();

而 gcTrigger 则是用的默认的 Default 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface GcTrigger {
  GcTrigger DEFAULT = new GcTrigger() {
    @Override public void runGc() {
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
      // java/lang/ref/FinalizationTester.java
      // System.gc() does not garbage collect every time. Runtime.gc() is
      // more likely to perfom a gc.
      Runtime.getRuntime().gc();
      enqueueReferences();
      System.runFinalization();
    }

    private void enqueueReferences() {
      // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
      // references to the appropriate queues.
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        throw new AssertionError();
      }
    }
  };

  void runGc();
}

代码中通过 Runtime.getRuntime().gc() 来触发 gc,再将当前线程 sleep 100 毫秒,来保证弱引用被添加至引用队列中,则执行 System.runFinalization() 方法,则触发调用对象的 finalize 方法。

再触发 gcTrigger.runGc() 后,再调用一次 removeWeaklyReachableReferences,若是对象还未消失,那表示对象已经是真的泄露了。则需要导出内存快照进行分析了。

4.Dump内存快照

生成 heapDumpFile, 则是由 heapDumper 接口来负责:

1
File heapDumpFile = heapDumper.dumpHeap();

此 heapDumper 在 Android 中相关的实现,是由 AndroidHeapDumper 类实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override public File dumpHeap() {
 File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();

 if (heapDumpFile == RETRY_LATER) {
   return RETRY_LATER;
 }

 FutureResult<Toast> waitingForToast = new FutureResult<>();
 showToast(waitingForToast);

 if (!waitingForToast.wait(5, SECONDS)) {
   CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
   return RETRY_LATER;
 }

 Toast toast = waitingForToast.get();
 try {
   Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
   cancelToast(toast);
   return heapDumpFile;
 } catch (Exception e) {
   CanaryLog.d(e, "Could not dump heap");
   // Abort heap dump
   return RETRY_LATER;
 }
}

因为这里是在主线程所做的 dump 工作,所以这里给了个 waiting 的 toast,之后便是利用 Debug 类中的 dumpHprofData 方法,来生成内存快照 hprof 的数据。

然后利用注册的 heapdumpListener 的 analyze 方法进行分析生成的此 HeapDump 对象:

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

这里会通过启动一个监听的 service(HeapAnalyzerService),而在 Android 中的 service 便是另一个进程的 serice。

5.引用分析

HeapAnalyzerService 是一个跨进程的 IntentSerice,在其 onHandleIntent 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
}

获取到 HeapDump 后,生成一个 HeapAnalyzer 后,利用其 checkForLeak 方法,检查 heapDumpFile 中关于 referenceKey 的引用,生成一个分析的结果 AnalysisResult。

关于 checkForLeak 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 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 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();
   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));
   }

   return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
 } catch (Throwable e) {
   return failure(e, since(analysisStartNanoTime));
 }
}

这里开始利用 square 的 haha 库来开始解析 dump 出来的文件数据了。首先将 heapDumpFile 包装为 HprofBuffer,再利用 HprofParse 转换得到 Snapshot 数据。

先利用 findLeakingReference 方法中,从 snapshot 中查看是否有 referenceKey。没有,则不存在泄露。

若有的话,在 findLeakTrace 方法,来找到关于此类引用泄露的路径关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 得到在 snapshot 中 leakingRef 的 findPath Result。通过 result 的 leakingNode 得到 LeakTrace。之后再将其封装到一个结果的类 AnalysisResult 中。

6.结果显示

之后将此结果 AnalysisResult 发送至 AbastractAnalysisResultService 的实现类中,此类的处理 Intent 方法如下:

1
2
3
4
5
6
7
8
9
10
@Override protected final void onHandleIntent(Intent intent) {
 HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAP_DUMP_EXTRA);
 AnalysisResult result = (AnalysisResult) intent.getSerializableExtra(RESULT_EXTRA);
 try {
   onHeapAnalyzed(heapDump, result);
 } finally {
   //noinspection ResultOfMethodCallIgnored
   heapDump.heapDumpFile.delete();
 }
}

其 onHeapAnalyzed 抽象方法由 DisplayLeakService 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
 String leakInfo = leakInfo(this, heapDump, result, true);
 CanaryLog.d("%s", leakInfo);

 boolean resultSaved = false;
 boolean shouldSaveResult = result.leakFound || result.failure != null;
 if (shouldSaveResult) {
   heapDump = renameHeapdump(heapDump);
   resultSaved = saveResult(heapDump, result);
 }

 PendingIntent pendingIntent;
 String contentTitle;
 String contentText;

 if (!shouldSaveResult) {
   contentTitle = getString(R.string.leak_canary_no_leak_title);
   contentText = getString(R.string.leak_canary_no_leak_text);
   pendingIntent = null;
 } else if (resultSaved) {
   pendingIntent = DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey);

   if (result.failure == null) {
     String size = formatShortFileSize(this, result.retainedHeapSize);
     String className = classSimpleName(result.className);
     if (result.excludedLeak) {
       contentTitle = getString(R.string.leak_canary_leak_excluded, className, size);
     } else {
       contentTitle = getString(R.string.leak_canary_class_has_leaked, className, size);
     }
   } else {
     contentTitle = getString(R.string.leak_canary_analysis_failed);
   }
   contentText = getString(R.string.leak_canary_notification_message);
 } else {
   contentTitle = getString(R.string.leak_canary_could_not_save_title);
   contentText = getString(R.string.leak_canary_could_not_save_text);
   pendingIntent = null;
 }
 // New notification id every second.
 int notificationId = (int) (SystemClock.uptimeMillis() / 1000);
 showNotification(this, contentTitle, contentText, pendingIntent, notificationId);
 afterDefaultHandling(heapDump, result, leakInfo);
}

这里检查获取的 AnalysisResult,根据其 leakFound 和 failure 来查看其是否泄漏。若存在,则创建一个跳转至 DisplayLeakActivity 的 Notification 的 intent。

在 DisplayLeakActivity 的界面中,用于显示关于 Leaks 的列表,而其 List 数据,则是通过运行一个后台线程来读取保存的泄露文件列表,之后更新 UI 显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Override public void run() {
final List<Leak> leaks = new ArrayList<>();
List<File> files = leakDirectoryProvider.listFiles(new FilenameFilter() {
  @Override public boolean accept(File dir, String filename) {
    return filename.endsWith(".result");
  }
});
for (File resultFile : files) {
  FileInputStream fis = null;
  try {
    fis = new FileInputStream(resultFile);
    ObjectInputStream ois = new ObjectInputStream(fis);
    HeapDump heapDump = (HeapDump) ois.readObject();
    AnalysisResult result = (AnalysisResult) ois.readObject();
    leaks.add(new Leak(heapDump, result, resultFile));
  } catch (IOException | ClassNotFoundException e) {
    // Likely a change in the serializable result class.
    // Let's remove the files, we can't read them anymore.
    boolean deleted = resultFile.delete();
    if (deleted) {
      CanaryLog.d(e, "Could not read result file %s, deleted it.", resultFile);
    } else {
      CanaryLog.d(e, "Could not read result file %s, could not delete it either.",
          resultFile);
    }
  } finally {
    if (fis != null) {
      try {
        fis.close();
      } catch (IOException ignored) {
      }
    }
  }
}
Collections.sort(leaks, new Comparator<Leak>() {
  @Override public int compare(Leak lhs, Leak rhs) {
    return Long.valueOf(rhs.resultFile.lastModified())
        .compareTo(lhs.resultFile.lastModified());
  }
});
mainHandler.post(new Runnable() {
  @Override public void run() {
    inFlight.remove(LoadLeaks.this);
    if (activityOrNull != null) {
      activityOrNull.leaks = leaks;
      activityOrNull.updateUi();
    }
  }
});
}

学习资料

版权归作者所有,转载请注明原文链接:/blog/2018/03/16/leakcanary-learn/

给 Ta 个打赏吧...

Comments

版权 © 2018 - alighters - Powered by Octopress