前面的两篇文章介绍了LeakCanary的核心架构和RefWatcher的工作原理,其核心内容就是查找泄漏对象的最短引用路径,这些工作都是在HeapAnalyzerService中完成的。
HeapAnalyzer
HeapAnalyzerService通过调用HeapAnalyzer的checkForLeak方法来进一步分析内存,使用HAHA将RefWatcher传递过来hprof文件解析成Snapshot对象,其中调用的方法包括SnapshotFactory的parse和HprofIndexBuilder的fill方法。解析得到的SnapShot对象和我们用MAT分析内存得出的对象结构类似,其源码如下:
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 |
public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) { ... ISnapshot snapshot = null; try { snapshot = openSnapshot(heapDumpFile); IObject leakingRef = findLeakingReference(referenceKey, snapshot); // False alarm, weak reference was cleared in between key check and heap dump. if (leakingRef == null) { return noLeak(since(analysisStartNanoTime)); } String className = leakingRef.getClazz().getName(); AnalysisResult result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true); if (!result.leakFound) { result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false); } return result; } catch (SnapshotException e) { return failure(e, since(analysisStartNanoTime)); } finally { cleanup(heapDumpFile, snapshot); } } |
它构建了一颗对象引用关系树,我们可以在这颗树中查询各个Object的信息,包括Class信息、内存地址、持有的引用以及被持有引用的关系。
找出泄漏对象
接下来的任务就是从Snapshot中找到一条有效的到泄漏对象之间的引用路径。通过调用findLeakTrace方法从Snapshot中找到泄漏对象,代码如下:
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 |
private IObject findLeakingReference(String key, ISnapshot snapshot) throws SnapshotException { Collection refClasses = snapshot.getClassesByName(KeyedWeakReference.class.getName(), false); if (refClasses.size() != 1) { throw new IllegalStateException( "Expecting one class for " + KeyedWeakReference.class.getName() + " in " + refClasses); } IClass refClass = refClasses.iterator().next(); int[] weakRefInstanceIds = refClass.getObjectIds(); for (int weakRefInstanceId : weakRefInstanceIds) { IObject weakRef = snapshot.getObject(weakRefInstanceId); String keyCandidate = PrettyPrinter.objectAsString((IObject) weakRef.resolveValue("key"), 100); if (keyCandidate.equals(key)) { // 匹配key return (IObject) weakRef.resolveValue("referent"); // 定位泄漏对象 } } throw new IllegalStateException("Could not find weak reference with key " + key); } |
为了能准确的找到泄漏对象,LeakCanary通过泄漏对象的WeakReference在Snapshot中定位它。因为,如果一个对象泄漏,一定可以在内存中找到这个对象的弱引用(这一切都是RefWatcher的功劳),再通过弱应用对象的reference就可以定位到泄漏对象。
寻找最短引用路径
找到泄漏对象之后,接下来就是找到一条到泄漏对象的最短引用路径,这个过程由findLeakTrace来完成,实际上寻找最短引用路径的逻辑是封装在PathsFromGCRootsComputerImpl类的getNextShortestPath和processCurrentReferrefs方法中,代码如下:
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 |
public int[] getNextShortestPath() throws SnapshotException { switch (state) { case 0: // INITIAL { ... } case 1: // FINAL return null; case 2: // PROCESSING GC ROOT { ... } case 3: // NORMAL PROCESSING { int[] res; // finish processing the current entry if (currentReferrers != null) { res = processCurrentReferrefs(lastReadReferrer + 1); if (res != null) return res; } // Continue with the FIFO while (fifo.size() > 0) { currentPath = fifo.getFirst(); fifo.removeFirst(); currentId = currentPath.getIndex(); currentReferrers = inboundIndex.get(currentId); if (currentReferrers != null) { res = processCurrentReferrefs(0); if (res != null) return res; } } return null; } default: ... } } private int[] processCurrentReferrefs(int fromIndex) throws SnapshotException { GCRootInfo[] rootInfo = null; for (int i = fromIndex; i < currentReferrers.length; i++) { ... } for (int referrer : currentReferrers) { if (referrer >= 0 && !visited.get(referrer) && !roots.containsKey(referrer)) { if (excludeMap == null) { fifo.add(new Path(referrer, currentPath)); visited.set(referrer); } else { if (!refersOnlyThroughExcluded(referrer, currentId)) { fifo.add(new Path(referrer, currentPath)); visited.set(referrer); } } } } return null; } } |
这个类将整个内存信息抽象为一个以GCRoot为根的树,由于之前已经定位了被泄漏对象在这颗树中的位置,为了找到到GCRoot的最短路径,PathsFromGCRootsComputerImpl采用类似广度优先的搜索策略,在getNextShortestPath中从被泄漏的对象开始,调用一次processCurrentReferrefs将持有它引用的节点(父节点),加入到一个FIFO队列中,然后依次再调用getNextShortestPath和processCurrentReferrefs来从FIFO中取节点及将这个节点的父节点再加入FIFO队列中,一层一层向上寻找,哪条路径最先到达GCRoot就表示它应该是一条最短路径。由于FIFO保存了查询信息,因此如果要找次最短路径只需要再调用一次getNextShortestPath触发下一次查找即可,其算法原理如下图所示。(配图来自百度手机助手团队——感谢)
接下来调用buildLeakTrace构建查询结果,将最短路径转换成最后用于显示的LeakTrace对象,它包括了路径上各节点LeakTraceElement组成的链表,代表了泄漏对象的最短路径。 最后将结果封装成AnalysisResult对象,传递到DisplayLeakService进行处理。这个service主要的工作是将检查结果写入文件,以便直接看到内存泄露的分析结果,同时以Notification的方式通知用户检测到了一次内存泄漏。使用者还可以继承这个service类来并实现afterDefaultHandling来自定义对检查结果的处理,比如将结果上传刚到服务器等。
总结
历时三个星期的LeakCanary源码分析系列文章就此告一段落。也是第一次写源码分析的文章,这种分析类型的文章看起来非常的枯燥晦涩,如果读者你看到了文章的结尾,那么我相信你一定是一个有耐心的程序员。今年的工资一定会翻倍。
最后送上彩蛋一枚,输入支付宝红包口令,领红包去吧~如果你觉得博客对你有用,你可以关注右侧我的微信公众号(android-it),也可以将文章分享给你的朋友