LeakCanary 是由 Square 開發的一款內存泄露檢測工具。相比與用 IDE dump memory 的繁瑣,它以輕便的日誌被廣大開發者所喜好。讓咱們看看它是如何實現的吧。java
ps: Square 以著名框架 Okhttp 被廣大開發者所熟知。api
分析一個框架,咱們能夠嘗試先分層。好的框架層次清晰,像TCP/IP那樣,一層一層的封裝起來。這裏,我按照主流程大體分了一下。架構
一圖流,你們能夠參考這個圖,來跟源碼。 oracle
按照教程,咱們一般會有以下初始化代碼:app
mRefWatcher = LeakCanary.install(this);
mRefWatcher.watch(this);
雖然是用戶端的代碼,不過做爲分析框架的入口,不妨稱爲業務層。框架
這一層咱們考慮的是檢測咱們的業務對象 Activity。固然你也能夠用來檢測 Service。dom
從業務層切入,咱們引出了兩個類LeakCanary
、RefWatcher
,組成了咱們的 api 層。異步
這一層咱們要考慮如何對外提供接口,並隱藏內部實現。一般會使用
Builder
、單例
、適當的包私有權限
。ide
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 新增的。
以前的分析,引出了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?線程池?咱們接着往下看
如今咱們來到了很是關鍵的一層,這一層主要是分析是否泄露,產物是.hprof文件
。 咱們日常用 IDE dump memory 的時候,生成的也是這種格式的文件。
接以前的分析,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()
,注意此時執行環境已經切到子線程了。
分析下一步以前,咱們先介紹一下 ReferenceQueue
。
說白了,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。
4-6. 引用還在,然而這裏沒有當即斷定爲泄漏,而是很謹慎的手動觸發 gc,再次校驗。
這裏注意一點 Android 下邊的 jdk 和 oracle 公司的 jdk 在一些方法的實現上有區別。好比這個 System.gc()
就被改了,再也不保證一定觸發 gc。做者使用Runtime.getRuntime().gc()
做爲代替。
瞭解更多:System.gc() 源碼解讀
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()
,生成了堆快照。
生成 .hprof
以後,以後由 heapdumpListener.analyze(heapDump)
把數據轉到下一層。其實這兩層沒啥好分析的,.hprof
已是標準的堆快照格式,平時用 AS 分析內存生成的也是這個格式。
因此,LeakCanary 在這一層只是幫咱們讀取了堆中的引用鏈。而後,日誌展現層也沒啥說的,就一個 ListView。
最後,咱們能夠看到一個優秀的框架須要那些東西:
分層
api層
有哪些接口以及業務層
怎麼使用;而對於維護者來講,不少時候只須要關心核心邏輯日誌產生層
,UI層不怎麼改動醜一點也不要緊。方便使用也方便維護。ReferenceQueue 的使用
new WeakReference()
。手動觸發 gc
Runtime.getRuntime().gc()
是否能當即觸發 gc,這點感受也比較含糊。這是一個 native 方法,依賴於 JVM 的實現,深究起來須要去看 Dalvik 的源碼,先打一個問號。AOSP
裏拷貝出來的。。。因此說,多看源碼是個好習慣。leakcanary-no-op