"Read the fucking source code" -- linus一句名言體現(xiàn)出了閱讀源碼的重要性,學(xué)習(xí)別人得代碼是提升自己的重要途徑覆糟。最近用到了LeakCanary,順便看一下其代碼,學(xué)習(xí)一下。
LeakCanary是安卓中用來(lái)檢測(cè)內(nèi)存泄露的小工具掸绞,它能幫助我們提早發(fā)現(xiàn)代碼中隱藏的bug, 降低應(yīng)用中內(nèi)存泄露以及OOM產(chǎn)生的概率。
廢話不多說(shuō)耕捞,關(guān)于LeakCanary的使用方法衔掸,其實(shí)很簡(jiǎn)單,如果我們只想檢測(cè)Activity的內(nèi)存泄露俺抽,而且只想使用其默認(rèn)的報(bào)告方式具篇,我們只需要在Application中加一行代碼,
LeakCanary.install(this);
那我們今天閱讀源碼的切入點(diǎn)凌埂,就從這個(gè)靜態(tài)方法開(kāi)始驱显。
/**
* Creates a {@link RefWatcher} that works out of the box, and starts watching activity
* references (on ICS+).
*/
public static RefWatcher install(Application application) {
return install(application, DisplayLeakService.class,
AndroidExcludedRefs.createAppDefaults().build());
}
這個(gè)函數(shù)內(nèi)部直接調(diào)用了另外一個(gè)重載的函數(shù)
/**
* Creates a {@link RefWatcher} that reports results to the provided service, and starts watching
* activity references (on ICS+).
*/
public static RefWatcher install(Application application,
Class<? extends AbstractAnalysisResultService> listenerServiceClass,
ExcludedRefs excludedRefs) {
//判斷是否在Analyzer進(jìn)程里
if (isInAnalyzerProcess(application)) {
return RefWatcher.DISABLED;
}
enableDisplayLeakActivity(application);
HeapDump.Listener heapDumpListener =
new ServiceHeapDumpListener(application, listenerServiceClass);
RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
return refWatcher;
}
因?yàn)閘eakcanay會(huì)開(kāi)啟一個(gè)遠(yuǎn)程service用來(lái)分析每次產(chǎn)生的內(nèi)存泄露,而安卓的應(yīng)用每次開(kāi)啟進(jìn)程都會(huì)調(diào)用Applicaiton的onCreate方法瞳抓,因此我們有必要預(yù)先判斷此次Application啟動(dòng)是不是在analyze service啟動(dòng)時(shí)埃疫,
public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
} catch (Exception e) {
Log.e("AndroidUtils", "Could not get package info for " + context.getPackageName(), e);
return false;
}
String mainProcess = packageInfo.applicationInfo.processName;
ComponentName component = new ComponentName(context, serviceClass);
ServiceInfo serviceInfo;
try {
serviceInfo = packageManager.getServiceInfo(component, 0);
} catch (PackageManager.NameNotFoundException ignored) {
// Service is disabled.
return false;
}
if (serviceInfo.processName.equals(mainProcess)) {
Log.e("AndroidUtils",
"Did not expect service " + serviceClass + " to run in main process " + mainProcess);
// Technically we are in the service process, but we're not in the service dedicated process.
return false;
}
//查找當(dāng)前進(jìn)程名
int myPid = android.os.Process.myPid();
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.RunningAppProcessInfo myProcess = null;
for (ActivityManager.RunningAppProcessInfo process : activityManager.getRunningAppProcesses()) {
if (process.pid == myPid) {
myProcess = process;
break;
}
}
if (myProcess == null) {
Log.e("AndroidUtils", "Could not find running process for " + myPid);
return false;
}
return myProcess.processName.equals(serviceInfo.processName);
}
判斷Application是否是在service進(jìn)程里面啟動(dòng),最直接的方法就是判斷當(dāng)前進(jìn)程名和service所屬的進(jìn)程是否相同孩哑。當(dāng)前進(jìn)程名的獲取方式是使用ActivityManager的getRunningAppProcessInfo方法栓霜,找到進(jìn)程pid與當(dāng)前進(jìn)程pid相同的進(jìn)程,然后從中拿到processName. service所屬進(jìn)程名横蜒。獲取service應(yīng)處進(jìn)程的方法是用PackageManager的getPackageInfo方法胳蛮。
RefWatcher
ReftWatcher是leakcancay檢測(cè)內(nèi)存泄露的發(fā)起點(diǎn)。使用方法為丛晌,在對(duì)象生命周期即將結(jié)束的時(shí)候仅炊,調(diào)用
RefWatcher.watch(Object object)
為了達(dá)到檢測(cè)內(nèi)存泄露的目的,RefWatcher需要
private final Executor watchExecutor;
private final DebuggerControl debuggerControl;
private final GcTrigger gcTrigger;
private final HeapDumper heapDumper;
private final Set<String> retainedKeys;
private final ReferenceQueue<Object> queue;
private final HeapDump.Listener heapdumpListener;
private final ExcludedRefs excludedRefs;
- watchExecutor: 執(zhí)行內(nèi)存泄露檢測(cè)的executor
- debuggerControl :用于查詢(xún)是否正在調(diào)試中澎蛛,調(diào)試中不會(huì)執(zhí)行內(nèi)存泄露檢測(cè)
- queue : 用于判斷弱引用所持有的對(duì)象是否已被GC抚垄。
- gcTrigger: 用于在判斷內(nèi)存泄露之前,再給一次GC的機(jī)會(huì)
- headDumper: 用于在產(chǎn)生內(nèi)存泄露室執(zhí)行dump 內(nèi)存heap
- heapdumpListener: 用于分析前面產(chǎn)生的dump文件谋逻,找到內(nèi)存泄露的原因
- excludedRefs: 用于排除某些系統(tǒng)bug導(dǎo)致的內(nèi)存泄露
- retainedKeys: 持有那些呆檢測(cè)以及產(chǎn)生內(nèi)存泄露的引用的key呆馁。
接下來(lái),我們來(lái)看看watch函數(shù)背后是如何利用這些工具毁兆,生成內(nèi)存泄露分析報(bào)告的浙滤。
public void watch(Object watchedReference, String referenceName) {
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
//如果處于debug模式,則直接返回
if (debuggerControl.isDebuggerAttached()) {
return;
}
//記住開(kāi)始觀測(cè)的時(shí)間
final long watchStartNanoTime = System.nanoTime();
//生成一個(gè)隨機(jī)的key气堕,并加入set中
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
//生成一個(gè)KeyedWeakReference
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
//調(diào)用watchExecutor纺腊,執(zhí)行內(nèi)存泄露的檢測(cè)
watchExecutor.execute(new Runnable() {
@Override public void run() {
ensureGone(reference, watchStartNanoTime);
}
});
}
所以最后的核心函數(shù)是在ensureGone這個(gè)runnable里面畔咧。要理解其工作原理,就得從keyedWeakReference說(shuō)起
WeakReference與ReferenceQueue
從watch函數(shù)中摹菠,可以看到盒卸,每次檢測(cè)對(duì)象內(nèi)存是否泄露時(shí)骗爆,我們都會(huì)生成一個(gè)KeyedReferenceQueue次氨,這個(gè)類(lèi)其實(shí)就是一個(gè)WeakReference,只不過(guò)其額外附帶了一個(gè)key和一個(gè)name
final class KeyedWeakReference extends WeakReference<Object> {
public final String 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");
}
}
在構(gòu)造時(shí)我們需要傳入一個(gè)ReferenceQueue摘投,這個(gè)ReferenceQueue是直接傳入了WeakReference中煮寡,關(guān)于這個(gè)類(lèi),有興趣的可以直接看Reference的源碼犀呼。我們這里需要知道的是幸撕,每次WeakReference所指向的對(duì)象被GC后,這個(gè)弱引用都會(huì)被放入這個(gè)與之相關(guān)聯(lián)的ReferenceQueue隊(duì)列中外臂。
我們這里可以貼下其核心代碼
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference<Object> r;
synchronized (lock) {
if (pending != null) {
r = pending;
pending = r.discovered;
r.discovered = null;
} else {
//....
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue<Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}
在reference類(lèi)加載的時(shí)候坐儿,java虛擬機(jī)會(huì)創(chuàng)建一個(gè)最大優(yōu)先級(jí)的后臺(tái)線程,這個(gè)線程的工作原理就是不斷檢測(cè)pending是否為null宋光,如果不為null貌矿,就將其放入ReferenceQueue中,pending不為null的情況就是罪佳,引用所指向的對(duì)象已被GC逛漫,變?yōu)椴豢蛇_(dá)。
那么只要我們?cè)跇?gòu)造弱引用的時(shí)候指定了ReferenceQueue赘艳,每當(dāng)弱引用所指向的對(duì)象被內(nèi)存回收的時(shí)候酌毡,我們就可以在queue中找到這個(gè)引用。如果我們期望一個(gè)對(duì)象被回收蕾管,那如果在接下來(lái)的預(yù)期時(shí)間之后枷踏,我們發(fā)現(xiàn)它依然沒(méi)有出現(xiàn)在ReferenceQueue中,那就可以判定它的內(nèi)存泄露了掰曾。LeakCanary檢測(cè)內(nèi)存泄露的核心原理就在這里呕寝。
其實(shí)Java里面的WeakHashMap里也用到了這種方法,來(lái)判斷hash表里的某個(gè)鍵值是否還有效婴梧。在構(gòu)造WeakReference的時(shí)候給其指定了ReferenceQueue.
監(jiān)測(cè)時(shí)機(jī)
什么時(shí)候去檢測(cè)能判定內(nèi)存泄露呢下梢?這個(gè)可以看AndroidWatchExecutor的實(shí)現(xiàn)
public final class AndroidWatchExecutor implements Executor {
//....
private void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
// This needs to be called from the main thread.
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
backgroundHandler.postDelayed(runnable, DELAY_MILLIS);
return false;
}
});
}
}
這里又看到一個(gè)比較少的用法,IdleHandler塞蹭,IdleHandler的原理就是在messageQueue因?yàn)榭臻e等待消息時(shí)給使用者一個(gè)hook孽江。那AndroidWatchExecutor會(huì)在主線程空閑的時(shí)候,派發(fā)一個(gè)后臺(tái)任務(wù)番电,這個(gè)后臺(tái)任務(wù)會(huì)在DELAY_MILLIS時(shí)間之后執(zhí)行岗屏。LeakCanary設(shè)置的是5秒辆琅。
二次確認(rèn)保證內(nèi)存泄露準(zhǔn)確性
為了避免因?yàn)間c不及時(shí)帶來(lái)的誤判,leakcanay會(huì)進(jìn)行二次確認(rèn)進(jìn)行保證这刷。
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
//計(jì)算從調(diào)用watch到進(jìn)行檢測(cè)的時(shí)間段
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//根據(jù)queue移除已被GC的對(duì)象的弱引用
removeWeaklyReachableReferences();
//如果內(nèi)存已被回收或者處于debug模式婉烟,直接返回
if (gone(reference) || debuggerControl.isDebuggerAttached()) {
return;
}
//如果內(nèi)存依舊沒(méi)被釋放,則再給一次gc的機(jī)會(huì)
gcTrigger.runGc();
//再次移除
removeWeaklyReachableReferences();
if (!gone(reference)) {
//走到這里暇屋,認(rèn)為內(nèi)存確實(shí)泄露了
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
//dump出heap報(bào)告
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == HeapDumper.NO_DUMP) {
// Could not dump the heap, abort.
return;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));
}
}
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);
}
}
Dump Heap
監(jiān)測(cè)到內(nèi)存泄露后似袁,首先做的就是dump出當(dāng)前的heap,默認(rèn)的AndroidHeapDumper調(diào)用的是
Debug.dumpHprofData(filePath);
到處當(dāng)前內(nèi)存的hprof分析文件咐刨,一般我們?cè)贒eviceMonitor中也可以dump出hprof文件昙衅,然后將其從dalvik格式轉(zhuǎn)成標(biāo)準(zhǔn)jvm格式,然后使用MAT進(jìn)行分析定鸟。
那么LeakCanary是如何分析內(nèi)存泄露的呢而涉?
HaHa
LeakCanary 分析內(nèi)存泄露用到了一個(gè)和Mat類(lèi)似的工具叫做HaHa,使用HaHa的方法如下:
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();
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));
}
}
關(guān)于HaHa的原理联予,感興趣的同學(xué)可以深究啼县,這里就不深入介紹了。
返回的ActivityResult對(duì)象中包含了對(duì)象到GC root的最短路徑沸久。LeakCanary在dump出hprof文件后季眷,會(huì)啟動(dòng)一個(gè)IntentService進(jìn)行分析:HeapAnalyzerService在分析出結(jié)果之后會(huì)啟動(dòng)DisplayLeakService用來(lái)發(fā)起Notification 以及將結(jié)果記錄下來(lái)寫(xiě)在文件里面。以后每次啟動(dòng)LeakAnalyzerActivity就從文件里讀取歷史結(jié)果麦向。
ExcludedRef
由于某些系統(tǒng)的bug瘟裸,以及某些廠商rom的bug,Activity在finish之后仍然會(huì)被某些系統(tǒng)組件給hold住诵竭。LeakCanary列出了一些很常見(jiàn)的话告,比如三星的手機(jī)activity會(huì)被audioManager給hold住,試了一下huawei的系統(tǒng)貌似也會(huì)出現(xiàn)卵慰,還有比如activity中如果有會(huì)獲取鍵盤(pán)焦點(diǎn)的view沙郭,在activity finish之后view會(huì)被InputMethodManager給hold住,因?yàn)関iew會(huì)持有activity 造成activity泄漏裳朋,除非有新的view獲取鍵盤(pán)焦點(diǎn)病线。
LeakCanary中有一個(gè)AndroidExcludedRefs枚舉類(lèi),其中枚舉了很多特定版本系統(tǒng)issue引起的內(nèi)存泄漏鲤嫡,因?yàn)檫@種問(wèn)題 不是開(kāi)發(fā)者導(dǎo)致的送挑,因此HeapAnalyzerService在分析內(nèi)存泄露時(shí),會(huì)將這些GC Root排除在外暖眼。而且每個(gè)ExcludedRef通常都跟特定廠商或者Android版本有關(guān)惕耕,這些枚舉類(lèi)都加了一個(gè)適用條件。
AndroidExcludedRefs(boolean applies) { this.applies = applies;}
AUDIO_MANAGER__MCONTEXT_STATIC(SAMSUNG.equals(MANUFACTURER) && SDK_INT == KITKAT) {
@Override void add(ExcludedRefs.Builder excluded) {
// Samsung added a static mContext_static field to AudioManager, holds a reference to the
// activity.
// Observed here: https://github.com/square/leakcanary/issues/32
excluded.staticField("android.media.AudioManager", "mContext_static");
}
},
比如上面這個(gè)AudioManager引起的問(wèn)題诫肠,只有在Build中的MANUFACTURER表明是三星以及sdk版本是KITKAT(4.4, 19)時(shí)才適用司澎。
手動(dòng)釋放資源
然后并不是leakCanary不報(bào)錯(cuò)我們就不用管欺缘,activity內(nèi)存泄露了,大部分情況下沒(méi)多大事挤安,但是有些占用內(nèi)存很多的頁(yè)面谚殊,比如圖庫(kù),webview頁(yè)面蛤铜,因?yàn)閍citivity不能回收嫩絮,它所指向的view以及view下面的bitmap都不能被回收,這是會(huì)造成很不好的后果的昂羡,很可能會(huì)導(dǎo)致OOM絮记,因此我們需要手動(dòng)在Activity結(jié)束時(shí)回收資源摔踱。
Under 4.0 & Fragment
LeakCanary只支持4.0以上虐先,原因是其中在watch 每個(gè)Activity時(shí)適用了Application的registerActivityLifecycleCallback函數(shù),這個(gè)函數(shù)只在4.0上才支持派敷,但是在4.0以下也是可以用的蛹批,可以在Application中將返回的RefWatcher存下來(lái),然后在基類(lèi)Activity的onDestroy函數(shù)中調(diào)用篮愉。
同理腐芍,如果我們想檢測(cè)Fragment的內(nèi)存的話,我們也闊以在Fragment的onDestroy中watch它试躏。