在对 hprof 文件进行处理前,首先需要对 hprof 文件格式有所了解。Android dump 的 hprof 文件和 java 的有一点点不一样,它在 java hprof 文件的基础上增加了几项,但文件格式是一样。所以需要先介绍下 java 的 hprof 文件格式,再介绍怎么处理 hprof 文件
hprof 文件格式
在 java 中,hprof 文件有2部分组成,一部分是 hprof head,一部分是 hprof body。其中 head 比较简单,由版本号,IDSize和时间组成。
body 的类型比较多,但是有规律,是由一系列的 Record 组成,由1个字节的Tag、4个字节的Time、4个字节的Length和Body组成,Tag表示该Record的类型,Body部分为该Record的内容,长度为Length。
以读取 STRING IN UTF8 为例
STRING IN UTF8 的结构如图:
final int tag = mStreamIn.read();
final int timestamp = IOUtil.readBEInt(mStreamIn);
final long length = IOUtil.readBEInt(mStreamIn) & 0x00000000FFFFFFFFL;
switch (tag) {
case HprofConstants.RECORD_TAG_STRING:
acceptStringRecord(timestamp, length, hv);
break;
private void acceptStringRecord(int timestamp, long length, HprofVisitor hv) throws IOException {
final ID id = IOUtil.readID(mStreamIn, mIdSize);
final String text = IOUtil.readString(mStreamIn, length - mIdSize);
hv.visitStringRecord(id, text, timestamp, length);
}
public static String readString(InputStream in, long length) throws IOException {
final byte[] buf = new byte[(int) length];
readFully(in, buf, 0, length);
return new String(buf, Charset.forName("UTF-8"));
}
public static void readFully(InputStream in, byte[] buf, int off, long length) throws IOException {
int n = 0;
while (n < length) {
final int count = in.read(buf, n, (int) (length - n));
if (count < 0) {
throw new EOFException();
}
n += count;
}
}
先读取1个字节 Tag、4个字节的 Time 和4个字节的 Length,之后再根据读取到的 tag 匹配相关的 Record,这里就是 tag == 0x01 来匹配的,匹配到是 STRING 之后再读取 length - ID 长度的内容为 value。到此 string 的解析完成。其余的也是相同的解析思路。
在 body 中有比较重要的是 HEAP DUMP 和 HEAP DUMP SEGMENT,这二块在 body 中占的比重比较大。而且 Android 在这里新增了android 特有的模块。
//传统模块
public static final int HEAPDUMP_ROOT_UNKNOWN = 0xff;
public static final int HEAPDUMP_ROOT_JNI_GLOBAL = 0x1;
public static final int HEAPDUMP_ROOT_JNI_LOCAL = 0x2;
public static final int HEAPDUMP_ROOT_JAVA_FRAME = 0x3;
public static final int HEAPDUMP_ROOT_NATIVE_STACK = 0x4;
public static final int HEAPDUMP_ROOT_STICKY_CLASS = 0x5;
public static final int HEAPDUMP_ROOT_THREAD_BLOCK = 0x6;
public static final int HEAPDUMP_ROOT_MONITOR_USED = 0x7;
public static final int HEAPDUMP_ROOT_THREAD_OBJECT = 0x8;
public static final int HEAPDUMP_ROOT_CLASS_DUMP = 0x20;
public static final int HEAPDUMP_ROOT_INSTANCE_DUMP = 0x21;
public static final int HEAPDUMP_ROOT_OBJECT_ARRAY_DUMP = 0x22;
public static final int HEAPDUMP_ROOT_PRIMITIVE_ARRAY_DUMP = 0x23;
//android 特有
public static final int HEAPDUMP_ROOT_HEAP_DUMP_INFO = 0xfe;
public static final int HEAPDUMP_ROOT_INTERNED_STRING = 0x89;
public static final int HEAPDUMP_ROOT_FINALIZING = 0x8a;
public static final int HEAPDUMP_ROOT_DEBUGGER = 0x8b;
public static final int HEAPDUMP_ROOT_REFERENCE_CLEANUP = 0x8c;
public static final int HEAPDUMP_ROOT_VM_INTERNAL = 0x8d;
public static final int HEAPDUMP_ROOT_JNI_MONITOR = 0x8e;
public static final int HEAPDUMP_ROOT_UNREACHABLE = 0x90; /* deprecated */
public static final int HEAPDUMP_ROOT_PRIMITIVE_ARRAY_NODATA_DUMP = 0xc3;
在网上及开发者网站上没有找到详细介绍,只能根据代码大概的写出来android特有的模块(基于 matrix 源码 com.tencent.matrix.resource.hproflib.HprofConstants,在art/runtime/hprof/hprof.cc 也有)
name | tag | value |
---|---|---|
ROOT HEAP DUMP INFO | 0xfe | heapId-U4 , heapNameId-ID |
ROOT INTERNED STRING | 0x89 | -ID |
ROOT FINALIZING | 0x8a | -ID |
ROOT DEBUGGER | 0x8b | -ID |
ROOT REFERENCE_CLEANUP | 0x8c | -ID |
ROOT VM INTERNAL | 0x8d | -ID |
ROOT JNI MONITOR | 0x8e | id-ID , threadSerialNumber-U4 , stackDepth-U4 |
ROOT UNREACHABLE | 0x90 | -ID |
ROOT PRIMITIVE ARRAY NODATA DUMP | 0xc3 | id-ID , stackId-U4 , numElements-U4 , typeId-U1 , elements-[numElements * ID] * U1 |
说明:value 中 name-size , name 是其名字(有可能没有),size 是其大小。
对于 body 中的 HEAP DUMP 和 HEAP DUMP SEGMENT 解析举个例子。
//1.先解析 record 的前三项,根据 tag == 0x0c || tag == 0x1c 来匹配 Record 的类型是 HEAP DUMP 或 HEAP DUMP SEGMENT
final int tag = mStreamIn.read();
final int timestamp = IOUtil.readBEInt(mStreamIn);
final long length = IOUtil.readBEInt(mStreamIn) & 0x00000000FFFFFFFFL;
switch (tag) {
case HprofConstants.RECORD_TAG_HEAP_DUMP:
case HprofConstants.RECORD_TAG_HEAP_DUMP_SEGMENT:
acceptHeapDumpRecord(tag, timestamp, length, hv);
break;
//2.再读出 1 个字节的子 tag == 0x02 来匹配 ROOT_JNI_LOCAL ,同时将上方的 length 减 1( length == 0 时表示 HEAP DUMP 或 HEAP DUMP SEGMENT 类型的 Record 解析完成)
final int heapDumpTag = mStreamIn.read();
--length;
switch (heapDumpTag) {
case HprofConstants.HEAPDUMP_ROOT_JNI_LOCAL:
length -= acceptJniLocal(hdv);
break;
//3.接着读出一个 IDSize 和 二个 U4 。表示 ROOT_JNI_LOCAL 子类型解析完毕
private int acceptJniLocal(HprofHeapDumpVisitor hdv) throws IOException {
final ID id = IOUtil.readID(mStreamIn, mIdSize);
final int threadSerialNumber = IOUtil.readBEInt(mStreamIn);
final int stackFrameNumber = IOUtil.readBEInt(mStreamIn);
hdv.visitHeapDumpJniLocal(id, threadSerialNumber, stackFrameNumber);
return mIdSize + 4 + 4;
}
处理 hprof 文件
处理 hprof 分为二类,,一是裁剪 hprof 文件,保留分析 OOM 的数据,使得在手机上的文件更容易的上传到后台分析。二是直接在手机上对 hprof 文件进行分析,中有用的信息。
1.裁剪 hprof 文件
hprof 文件中绝大部分数据是 PRIMITIVE ARRAY DUMP,通常占据80%以上,而分析 OOM 只关系对象的大小和引用关系,并不关心内容,因此这部分是裁剪的突破口。
- Matrix
Matrix 的 Hprof 文件裁剪功能的目标是将 Bitmap 和 String 以外的全部对象的基础类型数组的值移除,由于 Hprof 文件的分析功能只须要用到字符串数组和 Bitmap 的 buffer 数组。另外一方面,若是存在不一样的 Bitmap 对象其 buffer 数组值相同的状况,则能够将它们指向同一个 buffer,以进一步减少文件尺寸。
主要流程为
1.先寻找 bitmap、String、mBuffer、value 等类型的字符串索引。
2.从字符串索引中找到相应的类索引。
3.对于 bitmap 实例类会遍历其属性看是否有 mBuffer 与之匹配,如果有 2 次以上匹配的话就说明这个 bitmap 存在重复。对于 string 实例类会遍历其属性看是否有 value 与之匹配,匹配则记录到集合中。
4.最后在 array 写入时判断其 ID 是否等于步骤3中重复的 bitmap ID ,等于则不写入。或者判断其 ID 是否等于步骤3中 string 记录的集合中的 ID,不在集合中则写入。
- KOOM
KOOM 的裁剪方式和 Martix 不同,他是在 dump hprof 文件的过程中对文件的 open 和 write 进行了 plt hook,判断其是否需要裁剪写入,主要裁剪的也是 array 中的基本数据类型数组,避免了打开 hprof 文件时 OOM。
源码在 KOOM 的 hprof_strip.cpp 中实现。
koom 裁剪分析
2.分析 hprof 文件
有 haha 库或 shark 库可以很方便的分析 hprof 文件。
haha 分析 hprof 文件代码
- 主要流程
1.过滤重复的 GcRoot
2.从快照中获取泄露的 act 实例,没有拿到就说明没有泄露
3.广度遍历 GcRoot 的引用链,查看是否有与之匹配的 act 实例
// 开始分析的入口点在 HeapAnalyzerService
//HeapAnalyze
public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
@NonNull String referenceKey,
boolean computeRetainedSize) {
long analysisStartNanoTime = System.nanoTime();
...
try {
listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
listener.onProgressUpdate(PARSING_HEAP_DUMP);
Snapshot snapshot = parser.parse();
listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
// 过滤重复的 GcRoot
deduplicateGcRoots(snapshot);
listener.onProgressUpdate(FINDING_LEAKING_REF);
// 拿到泄露 obj 在快照中的实例
Instance leakingRef = findLeakingReference(referenceKey, snapshot);
// False alarm, weak reference was cleared in between key check and heap dump.
if (leakingRef == null) {
String className = leakingRef.getClassObj().getClassName();
return noLeak(className, since(analysisStartNanoTime));
}
// 广度遍历 GcRoot,查找泄露 leakingRef 与之遍历的 node 是否匹配
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
} catch (Throwable e) {
return failure(e, since(analysisStartNanoTime));
}
}
// 过滤重复的 GcRoot
void deduplicateGcRoots(Snapshot snapshot) {
// THashMap has a smaller memory footprint than HashMap.
final THashMap<String, RootObj> uniqueRootMap = new THashMap<>();
final Collection<RootObj> gcRoots = snapshot.getGCRoots();
for (RootObj root : gcRoots) {
String key = generateRootKey(root);
if (!uniqueRootMap.containsKey(key)) {
uniqueRootMap.put(key, root);
}
}
// Repopulate snapshot with unique GC roots.
gcRoots.clear();
uniqueRootMap.forEach(new TObjectProcedure<String>() {
@Override public boolean execute(String key) {
return gcRoots.add(uniqueRootMap.get(key));
}
});
}
private Instance findLeakingReference(String key, Snapshot snapshot) {
// KeyedWeakReference 包裹 activity 的类
ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
if (refClass == null) {
throw new IllegalStateException(
"Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
}
List<String> keysFound = new ArrayList<>();
for (Instance instance : refClass.getInstancesList()) {
List<ClassInstance.FieldValue> values = classInstanceValues(instance);
Object keyFieldValue = fieldValue(values, "key");
if (keyFieldValue == null) {
keysFound.add(null);
continue;
}
String keyCandidate = asString(keyFieldValue);
// 找到泄露 act
if (keyCandidate.equals(key)) {
return fieldValue(values, "referent");
}
keysFound.add(keyCandidate);
}
throw new IllegalStateException(
"Could not find weak reference with key " + key + " in " + keysFound);
}
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
Instance leakingRef, boolean computeRetainedSize) {
listener.onProgressUpdate(FINDING_SHORTEST_PATH);
ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
//广度遍历 GcRoot,查找泄露 leakingRef 与之遍历的 node 是否匹配
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
...
// 拿到泄露的路径
LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
// 返回
return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
since(analysisStartNanoTime));
}
Result findPath(Snapshot snapshot, Instance leakingRef) {
clearState();
canIgnoreStrings = !isString(leakingRef);
// GcRoot 入 toVisitQueue 队列,之后遍历
enqueueGcRoots(snapshot);
boolean excludingKnownLeaks = false;
LeakNode leakingNode = null;
while (!toVisitQueue.isEmpty() || !toVisitIfNoPathQueue.isEmpty()) {
LeakNode node;
if (!toVisitQueue.isEmpty()) {
// 取出队列头
node = toVisitQueue.poll();
} else {...}
// Termination
// 找到了
if (node.instance == leakingRef) {
leakingNode = node;
break;
}
// 将 node 的子节点入 toVisitQueue 队列,之后遍历
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 enqueueGcRoots(Snapshot snapshot) {
//遍历快照中所有的 GcRoot
for (RootObj rootObj : HahaSpy.allGcRoots(snapshot)) {
switch (rootObj.getRootType()) {
case JAVA_LOCAL:
...
break;
case INTERNED_STRING:
case DEBUGGER:
case INVALID_TYPE:
// An object that is unreachable from any other root, but not a root itself.
case UNREACHABLE:
case UNKNOWN:
// An object that is in a queue, waiting for a finalizer to run.
case FINALIZING:
break;
case SYSTEM_CLASS:
case VM_INTERNAL:
// A local variable in native code.
case NATIVE_LOCAL:
// A global variable in native code.
case NATIVE_STATIC:
// An object that was referenced from an active thread block.
case THREAD_BLOCK:
// Everything that called the wait() or notify() methods, or that is synchronized.
case BUSY_MONITOR:
case NATIVE_MONITOR:
case REFERENCE_CLEANUP:
// Input or output parameters in native code.
case NATIVE_STACK:
case JAVA_STATIC:
enqueue(null, null, rootObj, null);
break;
default:
throw new UnsupportedOperationException("Unknown root type:" + rootObj.getRootType());
}
}
}
private void enqueue(Exclusion exclusion, LeakNode parent, Instance child,
LeakReference leakReference) {
if (child == null) {
return;
}
...
LeakNode childNode = new LeakNode(exclusion, child, parent, leakReference);
if (visitNow) {
toVisitSet.add(child);
toVisitQueue.add(childNode);
} else {
toVisitIfNoPathSet.add(child);
toVisitIfNoPathQueue.add(childNode);
}
}
// visitClassObj visitClassObj visitClassInstance visitArrayInstance 类似
private void visitRootObj(LeakNode node) {
RootObj rootObj = (RootObj) node.instance;
Instance child = rootObj.getReferredInstance();
if (rootObj.getRootType() == RootType.JAVA_LOCAL) {...} else {
enqueue(null, node, child, null);
}
}