爲善不見其益,如草裏冬瓜,自能暗長;
爲惡不見其損,如庭前春雪,勢必潛消。
官網的一句話:千里之堤,潰於蟻穴
“A small leak will sink a great ship.” - Benjamin Franklin
LeakCanary 這個庫,作爲Android開發者應該都不會陌生。它的主要作用就是幫助我們找出內存泄漏的地方,幫助我們開發者減少OOM的發生。
1. LeakCanary簡述
我們正式進入LeakCanary的探索中。詳情請查看LeakCanary的文檔
通過查看LeakCanary的文檔,發現2.x的版本和1.x的版本差異還是比較大的。具體內容:升級到LeakCanary2
1.1 LeakCanary2的特點
- 新的堆分析工具,速度快且內存佔用率低; Shark
- 更新APIs來簡化配置,開放新的堆分析器的權限;
- 100% Kotlin 重寫;
- 一次分析過程可以探測多種泄漏類型,並按照類型分組;
心動不如行動,抓緊升級吧
2. LeakCanary 原理
由於項目中目前還在使用的是LeakCanary 1.x版本, 而 LeakCanary2的升級變化又比較大。這裏就簡單介紹一下1.x的版本,後續補上2.x版本的相關信息。
2.1 核心類 RefWatcher
1.1 RefWatcher所涉及的內容
public final class RefWatcher {
// 用來觀察泄漏情況的執行器,內部使用的是IdleHandler
private final WatchExecutor watchExecutor;
private final DebuggerControl debuggerControl;
// GC開關 -- 用來手動觸發GC操作的
private final GcTrigger gcTrigger;
// 用於導出堆信息文件
private final HeapDumper heapDumper;
// 用於監聽分析堆對象(HeapDump)的過程
private final HeapDump.Listener heapdumpListener;
// 堆對象的建造者
private final HeapDump.Builder heapDumpBuilder;
// 用於存儲回收的弱引用映射的Key
private final Set<String> retainedKeys;
// 引用隊列,
private final ReferenceQueue<Object> queue;
}
這麼多參數,而且都可以有默認實現—— 建造者模式
自然就產生 RefWatcherBuilder
又因爲我們主要在Android平臺上使用,所以Squareup 就又爲我們特有的AndroidRefWatcherBuilder
講到這裏,我們就應該明白我們的重點關注對象應該是:
- RefWatcher :判斷內存泄漏的相關邏輯都在這裏
- AndroidRefWatcherBuilder:針對於我們Android平臺,肯定有其獨特之處。
當然,對Builder模式感興趣的朋友可以關注一下RefWatcherBuilder的實現邏輯。
1.2 RefWatcher的功能實現
LeakCanary雖然總體思路跟我們差不多,但是具體實現還是很值得我們學習的。
1.2.1 監測功能
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
final long watchStartNanoTime = System.nanoTime();
// 通過UUID,給每個監測的引用對象綁定一個key
String key = UUID.randomUUID().toString();
// 添加集合,後面通過集合來判斷對應的引用是否被回收
retainedKeys.add(key);
// 注意這裏使用了引用隊列queu,果然不出你之所料
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
// 判斷引用是否能回收等相關操作(async 異步處理)
ensureGoneAsync(watchStartNanoTime, reference);
}
1.2.2 判斷及處理功能
如果發現其內存泄漏,我們可以自己觸發GC功能;若還是泄漏,我們應該分析並向開發者展示其泄漏相關信息。LeakCanary 也是這麼做的。(不服不行)
- ensureGoneAsync()
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
// 眼不眼熟,莫非是線程池?先保留懸念,如果是Java程序,肯定應該是(守護)線程池
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
// 真正的邏輯在這裏
return ensureGone(reference, watchStartNanoTime);
}
});
}
- ensureGone()
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
// 1. 遍歷引用隊列
removeWeaklyReachableReferences();
// 2. 判斷該引用是否被回收,若回收則直接返回
if (gone(reference)) {
return DONE;
}
// 該引用沒有被回收,手動觸發一次GC
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);
// 根據其文件,產生相應的堆內存分析對象HeapDump
HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
.referenceName(reference.name)
.watchDurationMs(watchDurationMs)
.gcDurationMs(gcDurationMs)
.heapDumpDurationMs(heapDumpDurationMs)
.build();
// 執行分析操作
heapdumpListener.analyze(heapDump);
}
return DONE;
}
- 相關方法
// 判斷該引用是否存在泄漏情況
private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}
// 如果沒有內存泄漏,則隊列中的元素不爲null,則遍歷集合刪除其對應的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);
}
}
文章到這裏,LeakCanary實現原理的整體邏輯基本呈現出來啦。不知道讀到這裏的朋友們有沒有疑問:
爲什麼要額外使用HastSet 呢? 不使用行不行呢?
首先我認爲不藉助於HashSet肯定是可以的,我們可以這樣實現:
private boolean removeReferences(KeyedWeakReference reference) {
KeyedWeakReference ref;
boolean result ;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
if(ref == reference){
result = true;
}
}
return result;
}
可能使用HashSet的目的是爲了反轉邏輯,更有利於理解吧。(PS:個人之愚見)
判斷內存泄漏
不使用HashSet:根據 ReferenceQueue 不存在其內容
使用HashSet : 根據 HashSet 存在其內容
2.2 HeadDump 堆內存分析對象
綜合考慮,還是建議把該類提前進行簡要分析一下
public final class HeapDump implements Serializable {
//1.用戶監聽堆對象分析過程的藉口
public interface Listener {
Listener NONE = new Listener() {
@Override public void analyze(HeapDump heapDump) {
}
};
void analyze(HeapDump heapDump);
}
/** The heap dump file, which you might want to upload somewhere. */
// 2. 正如註釋所講,有了此文件,自己也可以分析啊。(家有存糧,心中不慌)
public final File heapDumpFile;
// 3. 這兩個屬性,其實就是用於辨別泄漏對象用的。
public final String referenceKey;
public final String referenceName;
// 4. 劃重點,用於過濾那些不用被分析的應用的。(主次分明)
public final ExcludedRefs excludedRefs;
...
}
總結一下:
- 用於監聽分析的接口;
- 堆內存分析的文件;
- 識別相應泄漏引用的標示;
- 過濾不必要分析的東西;
- …
不得不感慨一下,就算知道怎麼寫,也思慮不周啊。
2.3 LeakCanary的總體脈絡
當我們真正掌握了引用隊列等相關知識後,我們就明白整個Java體系都可以利用引用隊列的特點來監測內存泄漏情況。通過閱讀LeakCanary的源碼,也印證了我們的猜想,可以看出來LeakCanary並不是只針對於Android平臺開發的。
這篇文章並不涉及Android的相關東西,這裏結合LeakCanary源碼對其進行一次概括性的總結。
2.3.1 人物
WatchExecutor :後臺監測內存泄漏;
RefWatcher :收集,處理並分析內存泄漏;
HeapDumper : 產生用於內存分析的文件;
HeapDump : 分析材料和結果;
ExcludedRefs :過濾不必分析的內容
2.3.2 過程
- 建造一個WatchExecutor(Java中大概率是線程池),在合適的時機監聽需要觀察的應用。
- RefWatcher 給每個引用通過UUID映射,並作出相關的收集,處理工作。如果處理不掉(內存泄漏),則利用HeapDumper生成文件File,並由文件產生HeapDump對象(若有不需要分析的,就藉助於ExcludedRef),通過其Listener進行分析並監聽分析結果。
至於用於分析堆內存信息,是藉助於haha庫,有興趣的朋友可以點進去詳細瞭解。PS : 該庫已廢棄.
下面就讓我們來見識一下:
LeakCanary在Android系統上所作的工作 剖析LeakCanary—— 下篇