1. 前言
LeakCanary 是由 Square 開發的一款記憶體洩露檢測工具。相比與用 IDE dump memory 的繁瑣,它以輕便的日誌被廣大開發者所喜愛。讓我們看看它是如何實現的吧。
ps: Square 以著名框架 Okhttp 被廣大開發者所熟知。
2. 原始碼分析
2.1 設計架構
分析一個框架,我們可以嘗試先分層。好的框架層次清晰,像TCP/IP那樣,一層一層的封裝起來。這裡,我按照主流程大致分了一下。
一圖流,大家可以參考這個圖,來跟原始碼。
2.2 業務層
按照教程,我們通常會有如下初始化程式碼:
- Applicaion 中:
mRefWatcher = LeakCanary.install(this);
- 基類 Activity/Fragment onDestory() 中:
mRefWatcher.watch(this);
雖然是使用者端的程式碼,不過作為分析框架的入口,不妨稱為業務層。
這一層我們考慮的是檢測我們的業務物件 Activity。當然你也可以用來檢測 Service。
2.3 Api層
從業務層切入,我們引出了兩個類LeakCanary
、RefWatcher
,組成了我們的 api 層。
這一層我們要考慮如何對外提供介面,並隱藏內部實現。通常會使用
Builder
、單例
、適當的包私有許可權
。
2.3.1 主線1 install()
public final class LeakCanary {
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);
}
}
複製程式碼
我們先看install()
,先拿到一個RefWatcherBuilder
,轉而使用Builder
模式構造一個RefWatcher
作為返回值。
大概可以知道是框架的一些初始配置。忽略其他,直接看buildAndInstall()
。
public final class AndroidRefWatcherBuilder extends RefWatcherBuilder<AndroidRefWatcherBuilder> {
...
private boolean watchActivities = true;
private boolean watchFragments = true;
public @NonNull RefWatcher buildAndInstall() {
RefWatcher refWatcher = build();
if (refWatcher != DISABLED) {
...
if (watchActivities) { // 1
ActivityRefWatcher.install(context, refWatcher);
}
if (watchFragments) { // 2
FragmentRefWatcher.Helper.install(context, refWatcher);
}
}
return refWatcher;
}
}
複製程式碼
可以看到 1, 2 兩處,預設行為是,監控 Activity 和 Fragment。 以 Activity為例:
public final class ActivityRefWatcher {
public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
...
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
};
}
複製程式碼
使用了Application.ActivityLifecycleCallbacks
,看來我們基類裡的watch()
是多餘的。Fragment 也是類似的,就不分析了,使用了FragmentManager.FragmentLifecycleCallbacks
。
PS: 老版本預設只監控 Activity,watchFragments 這個欄位是 2018/6 新增的。
2.3.2 主線2 watch()
之前的分析,引出了RefWatcher.watch()
,它可以檢測任意物件是否正常銷燬,不單單是 Activity。我們來分析看看:
public final class RefWatcher {
private final WatchExecutor watchExecutor;
public void watch(Object watchedReference, String referenceName) {
...
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
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);
}
});
}
}
複製程式碼
通過這個 watch(),我們可以注意到這幾點:
- 為了不阻塞我們的
onDestory()
,特意設計成非同步呼叫——WatchExecutor
。 - 有一個弱引用
KeyedWeakReference
,幹嘛用的呢?
我們該怎麼設計 WatchExecutor 呢?AsyncTask?執行緒池?我們接著往下看
2.4 日誌產生層
現在我們來到了非常關鍵的一層,這一層主要是分析是否洩露,產物是.hprof檔案
。
我們平常用 IDE dump memory 的時候,生成的也是這種格式的檔案。
2.4.1 WatchExecutor 非同步任務
接之前的分析,WatchExecutor
主要是用於非同步任務,同時提供了失敗重試的機制。
public final class AndroidWatchExecutor implements WatchExecutor {
private final Handler mainHandler;
private final Handler backgroundHandler;
public AndroidWatchExecutor(long initialDelayMillis) {
mainHandler = new Handler(Looper.getMainLooper());
HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
handlerThread.start();
backgroundHandler = new Handler(handlerThread.getLooper());
...
}
@Override public void execute(@NonNull Retryable retryable) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
waitForIdle(retryable, 0);
} else {
postWaitForIdle(retryable, 0);
}
}
...
}
複製程式碼
看來是使用了HandlerThread
。沒啥說的,要注意一下子執行緒Handler
的使用方式。之後便會回撥ensureGone()
,注意此時執行環境已經切到子執行緒了。
2.4.2 ReferenceQueue 檢測洩露
分析下一步之前,我們先介紹一下 ReferenceQueue
。
- 引用佇列 ReferenceQueue 作為引數傳入 WeakReference.
- WeakReference 中的 value 變得不可達,被 JVM 回收之前,WeakReference 會被加到該佇列中,等待回收。
說白了,ReferenceQueue 提供了一種通知機制,以便在 GC 發生前,我們能做一些處理。
好了,讓我們回到 RefWatcher。
final class KeyedWeakReference extends WeakReference<Object> {
public final String key; // 由於真正的 value 正等待回收,我們追加一個 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");
}
}
public final class RefWatcher {
private final Set<String> retainedKeys; // 儲存未回收的引用的 key。 watch()時 add, 在 queue 中找到則 remove。
private final ReferenceQueue<Object> queue; // 收集所有變得不可達的物件。
public void watch(Object watchedReference, String referenceName) {
...
String key = UUID.randomUUID().toString();
retainedKeys.add(key); // 1
final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
ensureGoneAsync(watchStartNanoTime, reference);
}
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
removeWeaklyReachableReferences(); // 2
...
if (gone(reference)) { // 3
return DONE;
}
gcTrigger.runGc(); // 4
removeWeaklyReachableReferences(); // 5
if (!gone(reference)) { // 6
// 發現洩漏
...
}
return DONE;
}
private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}
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);
}
}
}
複製程式碼
我們有這樣的策略:用retainedKeys
儲存未回收的引用的 key。
- 主執行緒 onDestroy() -> watch() -> retainedKeys.add(ref.key)。WatchExecutor 啟動,主執行緒 Activity 銷燬。
- WatchExecutor.execute() -> ensureGone() -> removeWeaklyReachableReferences() -> 遍歷 ReferenceQueue,從 retainedKeys.remove(ref.key)
- 判斷 gone(ref), 如果 Activity 已經不可達,那麼直接返回,否則可能有記憶體洩漏。
4-6. 引用還在,然而這裡沒有立即判定為洩漏,而是很謹慎的手動觸發 gc,再次校驗。
2.4.3 GcTrigger 手動觸發 Gc
這裡注意一點 Android 下邊的 jdk 和 oracle 公司的 jdk 在一些方法的實現上有區別。比如這個 System.gc()
就被改了,不再保證必定觸發 gc。作者使用Runtime.getRuntime().gc()
作為代替。
瞭解更多:System.gc() 原始碼解讀
2.4.4 HeapDumper 生成堆快照 .hprof
public final class RefWatcher {
private final HeapDumper heapDumper;
private final HeapDump.Listener heapdumpListener;
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
...
gcTrigger.runGc();
removeWeaklyReachableReferences();
if (!gone(reference)) {
// 發現洩漏
File heapDumpFile = heapDumper.dumpHeap();
HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile)
...
.build();
heapdumpListener.analyze(heapDump);
}
return DONE;
}
}
複製程式碼
我們跟進 heapDumper.dumpHeap(),略去一些 UI 相關程式碼:
public final class AndroidHeapDumper implements HeapDumper {
@Override @Nullable
public File dumpHeap() {
File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
...
try {
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
...
return heapDumpFile;
} catch (Exception e) { ... }
}
}
複製程式碼
最後用了 Android 原生的 api —— Debug.dumpHprofData()
,生成了堆快照。
2.5 日誌分析層 && 日誌展示層
生成 .hprof
之後,之後由 heapdumpListener.analyze(heapDump)
把資料轉到下一層。其實這兩層沒啥好分析的,.hprof
已經是標準的堆快照格式,平時用 AS 分析記憶體生成的也是這個格式。
所以,LeakCanary 在這一層只是幫我們讀取了堆中的引用鏈。然後,日誌展示層也沒啥說的,就一個 ListView。
3. 總結
最後,我們可以看到一個優秀的框架需要那些東西:
分層
- 分層的意義在於邏輯清晰,每一層的任務都很明確,儘量避免跨層的依賴,這符合單一職責的設計原則。
- 對於使用者來說,只用關心
api層
有哪些介面以及業務層
怎麼使用;而對於維護者來說,很多時候只需要關心核心邏輯日誌產生層
,UI層不怎麼改動醜一點也沒關係。方便使用也方便維護。
ReferenceQueue 的使用
- 學到了如何檢測記憶體回收情況,並且做一些處理。以前只會傻傻的
new WeakReference()
。
手動觸發 gc
Runtime.getRuntime().gc()
是否能立即觸發 gc,這點感覺也比較含糊。這是一個 native 方法,依賴於 JVM 的實現,深究起來需要去看 Dalvik 的原始碼,先打一個問號。- 框架中 gc 的這段程式碼是從
AOSP
裡拷貝出來的。。。所以說,多看原始碼是個好習慣。
leakcanary-no-op
- release 版提供一個空實現,可以學習一下。