为善不见其益,如草里冬瓜,自能暗长;
为恶不见其损,如庭前春雪,势必潜消。
官网的一句话:千里之堤,溃于蚁穴
“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—— 下篇