细节点 — 5
LeakCanary是如何找到泄漏的对象,以及最短的对象引用路径的呢?
功能的入口是在HeapAnalyzerService
里,调用到HeapAnalyzer
类:
@Override protected void onHandleIntent(Intent intent) {
if (intent == null) {
CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
return;
}
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
1、分析Dump文件
首先是调用HAHA的库分析Dump文件:
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
Snapshot snapshot = parser.parse();
Dump文件是来自上面HeapDumper
类。
文件的读取
MemoryMappedFileBuffer读取文件采用了NIO的channel的方式,这种的效率是很高的。
FileInputStream inputStream = new FileInputStream(f);
try {
long offset = 0;
for (int i = 0; i < shards; i++) {
long size = Math.min(mLength - offset, mBufferSize + mPadding);
mByteBuffers[i] = inputStream.getChannel()
.map(FileChannel.MapMode.READ_ONLY, offset, size);
mByteBuffers[i].order(HPROF_BYTE_ORDER);
offset += mBufferSize;
}
mCurrentPosition = 0;
} finally {
inputStream.close();
}
写了一个对比的文件读取代码:
// 采用chanel的方式
new Thread(new Runnable() {
@Override
public void run() {
File f = new File(MainActivity.this.getFilesDir(), "test");
try {
long start = System.currentTimeMillis();
FileInputStream inputStream = new FileInputStream(f);
mByteBuffers = inputStream.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, 20480000);
ByteBuffer b = mByteBuffers.asReadOnlyBuffer();
Log.e("111", (System.currentTimeMillis() - start) + "++++++++++++++");
String s = getString(b);
inputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
// 采用传统的方式
new Thread(new Runnable() {
@Override
public void run() {
File f = new File(MainActivity.this.getFilesDir(), "test");
try {
long start = System.currentTimeMillis();
FileInputStream inputStream = new FileInputStream(f);
byte[] b = new byte[20480000];
inputStream.read(b, 0, 20480000);
inputStream.close();
Log.e("111", (System.currentTimeMillis() - start) + "==============");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
测试的结果:
相应的,写文件采用channel的方式也是更快一些。
2、找出泄漏的对象
调用findLeakingReference()
方法。
核心的代码:
private Instance findLeakingReference(String key, Snapshot snapshot) {
ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
List<String> keysFound = new ArrayList<>();
for (Instance instance : refClass.getInstancesList()) {
List<ClassInstance.FieldValue> values = classInstanceValues(instance);
String keyCandidate = asString(fieldValue(values, "key"));
if (keyCandidate.equals(key)) {
return fieldValue(values, "referent");
}
keysFound.add(keyCandidate);
}
throw new IllegalStateException(
"Could not find weak reference with key " + key + " in " + keysFound);
}
在这里找到发生内存泄漏的对象,即相当于是根的对象。通过泄漏对象的WeakReference在Snaphot中定位它
3、寻找泄漏的引用的最短路径
在确定了泄漏的对象之后,接下来就要找到一条道泄漏对象的最短引用路径。这个是在findLeakTrace()
方法来完成。
大致的原理:
整个内存的信息被抽象成了以GCRoot为根的树状结构,由于已经知道了泄漏的对象,因此它在树中的位置是可以确定的。
因此采用广度优先的搜索策略,从被泄漏的对象开始,加入到FIFO队列中,一层层的往上面搜索,哪条路径最先到达GCRoot就表示这是最短路径。
上面的算法实现是在ShortestPathFinder
类里面,完成了上面最短路径后,接下来就调用buildLeakTrace构建查询结果,将最短路径转换成最后用于显示的LeakTrace对象,它包括了路径上各节点LeakTraceElement组成的链表,代表了泄漏对象的最短路径。 最后将结果封装成AnalysisResult对象,传递到DisplayLeakService进行处理。