LeakCanary 解析

“A small leak will sink a great ship.” - Benjamin Franklin

【目录】

离线配置

在线配置根据LeakCanary Github上的指引即可完成。不过因为公司一些网络上的限制,我只能使用离线配置来引入LeakCanary。

下载离线aar包及依赖文件

首先从Maven上下载LeakCanary的aar包以及依赖的jar包,共有以下五个文件:

加入项目中

在工程中建立一个目录,可以跟srcres等目录同级,比如YourProject/leakcanary/,然后将上述五个文件放到此目录下。

编辑gradle文件

在Module的build.gradle中加入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
repositories {
flatDir {
// 这个路径就是上一步中建立的文件夹的相对路径
dirs 'leakcanary'
}
}

dependencies {
debugCompile fileTree(dir: 'leakcanary', include: '*.jar')
// 以下两句的name是指文件名,具体名称视你的文件名而定。
debugCompile (name: 'com.squareup.leakcanary-android', ext: 'aar')
releaseCompile (name: 'com.squareup.leakcanary-android-no-op', ext: 'aar')
}

源码分析

对 LeakCanary 的源码进行分析。

判断是否泄露

先上一张时序图:

SequenceDiagram
其关键代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/** RefWatcher.java */

/** 当我们要监视一个对象是否存在泄露时,会调用这个方法 */
public void watch(Object watchedReference) {
watch(watchedReference, "");
}

public void watch(Object watchedReference, String referenceName) {
// ...
// 随机生成一个key,保证key的唯一性
String key = UUID.randomUUID().toString();
// 将key保存在 Set 中
retainedKeys.add(key);
// 创建WeakReference,key是联系 reference 对象和 retainedKeys 的纽带
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);
}
});
}

/** 检查是否存在泄漏 */
@SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
// ...
// 首先将已回收的对象对应的key从retainedKeys中移除
removeWeaklyReachableReferences();

// 如果正在debug断点调试,则延迟执行检查(因为断点会影响准确性)
if (debuggerControl.isDebuggerAttached()) {
// The debugger can create false leaks.
return RETRY;
}
// 通过判断retainedKeys中是否还包含当前的key来判断当前对象是否被回收,
// 如果已被回收,则返回DONE
if (gone(reference)) {
return DONE;
}
// 如果没有被回收,则触发一次GC
gcTrigger.runGc();
// 再次将已回收的对象对应的key从retainedKeys中移除
removeWeaklyReachableReferences();
// 再次判断是否被回收
// 如果被回收则返回DONE,结束
// 如果没有被回收,则需要查找造成泄漏的引用链中最短的一条(一个对象可能被多个对象引用,所以造成泄漏的地方可能不止一处)
if (!gone(reference)) {
// ...
// 获取堆内存镜像文件,最终调用的是Debug#dumpHprofData(String)方法来获取内存镜像
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.
return RETRY;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
// 启动分析服务
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));
}
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);
}
}

查找最短强引用路径

查找最短强引用路径的流程如下图:
Shortest Strong Reference Path

首先来看一下分析服务是如何启动的:

1
2
3
4
5
/** RefWatcher#ensureGone(KeyedWeakReference, long) */
// 启动分析服务
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));

上述代码执行后,会调用ServiceHeapDumpListener#analyze(HeapDump),继而调用HeapAnalyzerService#runAnalysis(Context, HeapDump, Class<? extends AbstractAnalysisResultService>)

1
2
3
4
5
6
7
public static void runAnalysis(Context context, HeapDump heapDump,
Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
Intent intent = new Intent(context, HeapAnalyzerService.class);
intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
intent.putExtra(HEAPDUMP_EXTRA, heapDump);
context.startService(intent);
}

HeapAnalyzerService是一个IntentService,来看一下它的onHandleIntent方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override protected void onHandleIntent(Intent intent) {
// ...
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

// 创建HeapAnalyzer
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);

// 调用HeapAnalyzer分析内存泄露路径
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
// 发送结果
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}

HeapAnalyzer#checkForLeak(File, String)是查找最短强引用路径的开端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 使用上文提到的使用UUID.randomUUID()方法生成的key来获取到泄露的对象实例,然后
* 计算从该实例到达GC Roots的最短强引用路径
*/
public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
// ...
try {
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
Snapshot snapshot = parser.parse();
// 从内存镜像中获取所有的GC Roots,并将它们添加到一个集合中
deduplicateGcRoots(snapshot);

// 使用反射,通过key找到泄露的对象实例
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));
}
}

private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot, Instance leakingRef) {
// ShortestPathFinder类是查找最短强引用路径的核心
ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

// 根据查找的结果,建立泄露路径
LeakTrace leakTrace = buildLeakTrace(result.leakingNode);

String className = leakingRef.getClassObj().getClassName();

// Side effect: computes retained size.
snapshot.computeDominators();

Instance leakingInstance = result.leakingNode.instance;

long retainedSize = leakingInstance.getTotalRetainedSize();

// TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
if (SDK_INT <= N_MR1) {
retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
}

return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
since(analysisStartNanoTime));
}

ShortestPathFinder#findPath(Snapshot, Instance)查找最短强引用路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
Result findPath(Snapshot snapshot, Instance leakingRef) {
// ...
// 将上面找到的所有GC Roots添加到队列中
enqueueGcRoots(snapshot);
// ...
LeakNode leakingNode = null;
// 如果将从GC Root开始的所有引用看做树,则这里就可以理解成使用广度优先搜索遍历引用“森林”
// 如果将所有的引用都看做是长度为1的Edge,那么这些引用就组成了一幅有向图,
// 这里就是使用类似Dijkstra算法的方法来寻找最短路径,越在队列后面的,距离GC Roots越远
while (!toVisitQueue.isEmpty() || !toVisitIfNoPathQueue.isEmpty()) {
LeakNode node;
// 如果toVisitQueue中没有元素,则取toVisitIfNoPathQueue中的元素
// 意思就是,如果遍历完了toVisitQueue还没有找到泄露的路径,那么就继续遍历设置了“例外”的那些对象
// “例外”是什么情况?在后续两个方法会讲到。
if (!toVisitQueue.isEmpty()) {
node = toVisitQueue.poll();
} else {
node = toVisitIfNoPathQueue.poll();
if (node.exclusion == null) {
throw new IllegalStateException("Expected node to have an exclusion " + node);
}
excludingKnownLeaks = true;
}

// Termination
if (node.instance == leakingRef) {
leakingNode = node;
break;
}

// 因为一个对象可以被多个对象引用,以GC Root为根的引用树
// 并不是严格意义上的树,所以如果已经遍历过当前对象,就跳过
if (checkSeen(node)) {
continue;
}

// 下面是根据当前引用节点的类型,分别找到它们所引用的对象
if (node.instance instanceof RootObj) {
visitRootObj(node);
} else if (node.instance instanceof ClassObj) {
visitClassObj(node);
} else if (node.instance instanceof ClassInstance) {
visitClassInstance(node);
} else if (node.instance instanceof ArrayInstance) {
visitArrayInstance(node);
} else {
throw new IllegalStateException("Unexpected type for " + node.instance);
}
}
// 返回查找结果
return new Result(leakingNode, excludingKnownLeaks);
}

/**
* 以类对象为例,会找到当前实例的所有对象成员,将它们添加到队列中。
* 至于其他对象的遍历,大家感兴趣的话可以自行研究。
*/
private void visitClassInstance(LeakNode node) {
ClassInstance classInstance = (ClassInstance) node.instance;
Map<String, Exclusion> ignoredFields = new LinkedHashMap<>();
ClassObj superClassObj = classInstance.getClassObj();
Exclusion classExclusion = null;
// 将设置了“例外”的对象记录下来
// 这里的“例外”就是上一个方法中提到的“例外”。是指那些低优先级的,或者说几乎不可能引发内存泄露的对象
// 例如SDK中的一些对象,诸如Message, InputMethodManager等,一般情况下,这些对象都不会导致内存泄露。
// 因此只有在遍历了其他对象之后,找不到泄露路径的情况下,才遍历这些对象。
while (superClassObj != null) {
Exclusion params = excludedRefs.classNames.get(superClassObj.getClassName());
if (params != null) {
// true overrides null or false.
// 如果当前类或者其父类被设置了“例外”,则将其赋值给classExclusion
if (classExclusion == null || !classExclusion.alwaysExclude) {
classExclusion = params;
}
}

// 如果当前类及其父类包含例外的成员,将这些成员添加到ignoredFields中
Map<String, Exclusion> classIgnoredFields =
excludedRefs.fieldNameByClassName.get(superClassObj.getClassName());
if (classIgnoredFields != null) {
ignoredFields.putAll(classIgnoredFields);
}
superClassObj = superClassObj.getSuperClassObj();
}

if (classExclusion != null && classExclusion.alwaysExclude) {
return;
}

// 遍历每一个成员
for (ClassInstance.FieldValue fieldValue : classInstance.getValues()) {
Exclusion fieldExclusion = classExclusion;
Field field = fieldValue.getField();
// 如果成员不是对象,则忽略
if (field.getType() != Type.OBJECT) {
continue;
}
// 获取成员实例
Instance child = (Instance) fieldValue.getValue();
String fieldName = field.getName();
Exclusion params = ignoredFields.get(fieldName);
// 如果当前成员对象是例外的,并且当前类和所有父类都不是例外的(classExclusion = null),
// 或,如果当前成员对象时例外的,而且是alwaysExclude,而且当前类和父类都不是alwaysExclude
// 则认为当前成员是需要例外处理的。
// 这个逻辑很绕,实际上“||”后面的判断是不需要的,具体在enqueue方法中讲。
if (params != null && (fieldExclusion == null || (params.alwaysExclude
&& !fieldExclusion.alwaysExclude))) {
fieldExclusion = params;
}

// 入队列
enqueue(fieldExclusion, node, child, fieldName, INSTANCE_FIELD);
}
}

private void enqueue(Exclusion exclusion, LeakNode parent, Instance child, String referenceName,
LeakTraceElement.Type referenceType) {
if (child == null) {
return;
}
if (isPrimitiveOrWrapperArray(child) || isPrimitiveWrapper(child)) {
return;
}
// Whether we want to visit now or later, we should skip if this is already to visit.
if (toVisitSet.contains(child)) {
return;
}

// 这个exclusion就是上一个方法通过“很绕的”逻辑判断的出来的
// 这里的作用就是如果为null则visitNow,这个boolean值在下面会用到。
// 可以看到这里只是判断exclusion是否为null,并没有使用到alwaysExclude参数,
// 所以说上一个方法中,“||”之后的判断是没有必要的。
boolean visitNow = exclusion == null;
if (!visitNow && toVisitIfNoPathSet.contains(child)) {
return;
}
if (canIgnoreStrings && isString(child)) {
return;
}
if (visitedSet.contains(child)) {
return;
}
LeakNode childNode = new LeakNode(exclusion, child, parent, referenceName, referenceType);
// 这里用到了boolean值visitNow,就是说如果exclusion对象为null,则表示这不是一个例外的对象(暂且称之为常规对象);
// 如果exclusion对象不为null,则表示这个对象是例外对象,只有在遍历所有常规对象之后,还是找不到路径的情况下才会被遍历。
if (visitNow) {
toVisitSet.add(child);
toVisitQueue.add(childNode);
} else {
toVisitIfNoPathSet.add(child);
toVisitIfNoPathQueue.add(childNode);
}
}

基本代码逻辑分析完毕。

附录

Q: LeakCanary为什么可以自动监视Activity?

A: 答案在于Application.ActivityLifecycleCallbacks接口。具体可以查看ActivityRefWatcher#watchActivities()

Q: LeakCanary是在onDestroy()方法中对Activity添加监视。因为onDestroy()方法执行后,Instrumentation或其他一些类并没有立即释放对该Activity的强引用,那么它怎么保证得到正确的结果?

A: LeakCanary是“异步”调用ensureGone()方法的。这个异步利用了IDLEHandler的原理。具体代码可查看AndroidWatchExecutor类中的相关方法。