LeakCanary 源码分析
1. 前言
LeakCanary 是由 Square 開(kāi)發(fā)的一款內(nèi)存泄露檢測(cè)工具。相比與用 IDE dump memory 的繁瑣,它以輕便的日志被廣大開(kāi)發(fā)者所喜愛(ài)。讓我們看看它是如何實(shí)現(xiàn)的吧。
ps: Square 以著名框架 Okhttp 被廣大開(kāi)發(fā)者所熟知。
2. 源碼分析
2.1 設(shè)計(jì)架構(gòu)
分析一個(gè)框架,我們可以嘗試先分層。好的框架層次清晰,像TCP/IP那樣,一層一層的封裝起來(lái)。這里,我按照主流程大致分了一下。
一圖流,大家可以參考這個(gè)圖,來(lái)跟源碼。
2.2 業(yè)務(wù)層
按照教程,我們通常會(huì)有如下初始化代碼:
雖然是用戶端的代碼,不過(guò)作為分析框架的入口,不妨稱(chēng)為業(yè)務(wù)層。
這一層我們考慮的是檢測(cè)我們的業(yè)務(wù)對(duì)象 Activity。當(dāng)然你也可以用來(lái)檢測(cè) Service。
2.3 Api層
從業(yè)務(wù)層切入,我們引出了兩個(gè)類(lèi)LeakCanary、RefWatcher,組成了我們的 api 層。
這一層我們要考慮如何對(duì)外提供接口,并隱藏內(nèi)部實(shí)現(xiàn)。通常會(huì)使用 Builder、單例、適當(dāng)?shù)陌接袡?quán)限。
2.3.1 主線1 install()
public final class LeakCanary {public static RefWatcher install(@NonNull Application application) {return refWatcher(application).listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build()).buildAndInstall();}public static AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {return new AndroidRefWatcherBuilder(context);} } 復(fù)制代碼我們先看install(),先拿到一個(gè)RefWatcherBuilder,轉(zhuǎn)而使用Builder模式構(gòu)造一個(gè)RefWatcher作為返回值。 大概可以知道是框架的一些初始配置。忽略其他,直接看buildAndInstall()。
public final class AndroidRefWatcherBuilder extends RefWatcherBuilder<AndroidRefWatcherBuilder> {...private boolean watchActivities = true;private boolean watchFragments = true;public RefWatcher buildAndInstall() {RefWatcher refWatcher = build();if (refWatcher != DISABLED) {...if (watchActivities) { // 1ActivityRefWatcher.install(context, refWatcher);}if (watchFragments) { // 2FragmentRefWatcher.Helper.install(context, refWatcher);}}return refWatcher;} } 復(fù)制代碼可以看到 1, 2 兩處,默認(rèn)行為是,監(jiān)控 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() { public void onActivityDestroyed(Activity activity) {refWatcher.watch(activity);}}; } 復(fù)制代碼使用了Application.ActivityLifecycleCallbacks,看來(lái)我們基類(lèi)里的watch()是多余的。Fragment 也是類(lèi)似的,就不分析了,使用了FragmentManager.FragmentLifecycleCallbacks。
PS: 老版本默認(rèn)只監(jiān)控 Activity,watchFragments 這個(gè)字段是 2018/6 新增的。
2.3.2 主線2 watch()
之前的分析,引出了RefWatcher.watch(),它可以檢測(cè)任意對(duì)象是否正常銷(xiāo)毀,不單單是 Activity。我們來(lái)分析看看:
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() { public Retryable.Result run() {return ensureGone(reference, watchStartNanoTime);}});} } 復(fù)制代碼通過(guò)這個(gè) watch(),我們可以注意到這幾點(diǎn):
我們?cè)撛趺丛O(shè)計(jì) WatchExecutor 呢?AsyncTask?線程池?我們接著往下看
2.4 日志產(chǎn)生層
現(xiàn)在我們來(lái)到了非常關(guān)鍵的一層,這一層主要是分析是否泄露,產(chǎn)物是.hprof文件。 我們平常用 IDE dump memory 的時(shí)候,生成的也是這種格式的文件。
2.4.1 WatchExecutor 異步任務(wù)
接之前的分析,WatchExecutor主要是用于異步任務(wù),同時(shí)提供了失敗重試的機(jī)制。
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());...} public void execute(@NonNull Retryable retryable) {if (Looper.getMainLooper().getThread() == Thread.currentThread()) {waitForIdle(retryable, 0);} else {postWaitForIdle(retryable, 0);}}... } 復(fù)制代碼看來(lái)是使用了HandlerThread。沒(méi)啥說(shuō)的,要注意一下子線程Handler的使用方式。之后便會(huì)回調(diào)ensureGone(),注意此時(shí)執(zhí)行環(huán)境已經(jīng)切到子線程了。
2.4.2 ReferenceQueue 檢測(cè)泄露
分析下一步之前,我們先介紹一下 ReferenceQueue。
說(shuō)白了,ReferenceQueue 提供了一種通知機(jī)制,以便在 GC 發(fā)生前,我們能做一些處理。
詳見(jiàn) Reference 、ReferenceQueue 詳解
好了,讓我們回到 RefWatcher。
final class KeyedWeakReference extends WeakReference<Object> {public final String key; // 由于真正的 value 正等待回收,我們追加一個(gè) key 來(lái)識(shí)別目標(biāo)。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()時(shí) add, 在 queue 中找到則 remove。private final ReferenceQueue<Object> queue; // 收集所有變得不可達(dá)的對(duì)象。public void watch(Object watchedReference, String referenceName) {...String key = UUID.randomUUID().toString();retainedKeys.add(key); // 1final 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)) { // 3return DONE;}gcTrigger.runGc(); // 4removeWeaklyReachableReferences(); // 5if (!gone(reference)) { // 6// 發(fā)現(xiàn)泄漏...}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);}} } 復(fù)制代碼我們有這樣的策略:用retainedKeys保存未回收的引用的 key。
4-6. 引用還在,然而這里沒(méi)有立即判定為泄漏,而是很謹(jǐn)慎的手動(dòng)觸發(fā) gc,再次校驗(yàn)。
2.4.3 GcTrigger 手動(dòng)觸發(fā) Gc
這里注意一點(diǎn) Android 下邊的 jdk 和 oracle 公司的 jdk 在一些方法的實(shí)現(xiàn)上有區(qū)別。比如這個(gè) System.gc()就被改了,不再保證必定觸發(fā) 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)) { // 發(fā)現(xiàn)泄漏File heapDumpFile = heapDumper.dumpHeap();HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile)....build();heapdumpListener.analyze(heapDump);}return DONE;} } 復(fù)制代碼我們跟進(jìn) heapDumper.dumpHeap(),略去一些 UI 相關(guān)代碼:
public final class AndroidHeapDumper implements HeapDumper { public File dumpHeap() {File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();...try {Debug.dumpHprofData(heapDumpFile.getAbsolutePath());...return heapDumpFile;} catch (Exception e) { ... }} } 復(fù)制代碼最后用了 Android 原生的 api —— Debug.dumpHprofData(),生成了堆快照。
2.5 日志分析層 && 日志展示層
生成 .hprof 之后,之后由 heapdumpListener.analyze(heapDump) 把數(shù)據(jù)轉(zhuǎn)到下一層。其實(shí)這兩層沒(méi)啥好分析的,.hprof 已經(jīng)是標(biāo)準(zhǔn)的堆快照格式,平時(shí)用 AS 分析內(nèi)存生成的也是這個(gè)格式。
所以,LeakCanary 在這一層只是幫我們讀取了堆中的引用鏈。然后,日志展示層也沒(méi)啥說(shuō)的,就一個(gè) ListView。
3. 總結(jié)
最后,我們可以看到一個(gè)優(yōu)秀的框架需要那些東西:
分層
- 分層的意義在于邏輯清晰,每一層的任務(wù)都很明確,盡量避免跨層的依賴,這符合單一職責(zé)的設(shè)計(jì)原則。
- 對(duì)于使用者來(lái)說(shuō),只用關(guān)心api層有哪些接口以及業(yè)務(wù)層怎么使用;而對(duì)于維護(hù)者來(lái)說(shuō),很多時(shí)候只需要關(guān)心核心邏輯日志產(chǎn)生層,UI層不怎么改動(dòng)丑一點(diǎn)也沒(méi)關(guān)系。方便使用也方便維護(hù)。
ReferenceQueue 的使用
- 學(xué)到了如何檢測(cè)內(nèi)存回收情況,并且做一些處理。以前只會(huì)傻傻的new WeakReference()。
手動(dòng)觸發(fā) gc
- Runtime.getRuntime().gc() 是否能立即觸發(fā) gc,這點(diǎn)感覺(jué)也比較含糊。這是一個(gè) native 方法,依賴于 JVM 的實(shí)現(xiàn),深究起來(lái)需要去看 Dalvik 的源碼,先打一個(gè)問(wèn)號(hào)。
- 框架中 gc 的這段代碼是從 AOSP 里拷貝出來(lái)的。。。所以說(shuō),多看源碼是個(gè)好習(xí)慣。
leakcanary-no-op
- release 版提供一個(gè)空實(shí)現(xiàn),可以學(xué)習(xí)一下。
總結(jié)
以上是生活随笔為你收集整理的LeakCanary 源码分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 使用CMD命令删除文件函数
- 下一篇: java B2B2C springmvc