Hprof 文件格式
Hprof 文件使用的基本数据类型为:u1、u2、u4、u8,分别表示 1 byte、2 byte、4 byte、8 byte 的内容,由文件头和文件内容两部分组成。
其中,文件头包含以下信息:
长度 | 含义 |
---|---|
[u1]* | 以 null 结尾的一串字节,用于表示格式名称及版本,比如 JAVA PROFILE 1.0.1(由 18 个 u1 字节组成) |
u4 | size of identifiers,即字符串、对象、堆栈等信息的 id 的长度(很多 record 的具体信息需要通过 id 来查找) |
u8 | 时间戳,时间戳,1970/1/1 以来的毫秒数 |
文件内容由一系列 records 组成,每一个 record 包含如下信息:
长度 | 含义 |
---|---|
u1 | TAG,表示 record 类型 |
u4 | TIME,时间戳,相对文件头中的时间戳的毫秒数 |
u4 | LENGTH,即 BODY 的字节长度 |
u4 | BODY,具体内容 |
查看 hprof.cc 可知,Hprof 文件定义的 TAG 有:
enum HprofTag {
HPROF_TAG_STRING = 0x01, // 字符串
HPROF_TAG_LOAD_CLASS = 0x02, // 类
HPROF_TAG_UNLOAD_CLASS = 0x03,
HPROF_TAG_STACK_FRAME = 0x04, // 栈帧
HPROF_TAG_STACK_TRACE = 0x05, // 堆栈
HPROF_TAG_ALLOC_SITES = 0x06,
HPROF_TAG_HEAP_SUMMARY = 0x07,
HPROF_TAG_START_THREAD = 0x0A,
HPROF_TAG_END_THREAD = 0x0B,
HPROF_TAG_HEAP_DUMP = 0x0C, // 堆
HPROF_TAG_HEAP_DUMP_SEGMENT = 0x1C,
HPROF_TAG_HEAP_DUMP_END = 0x2C,
HPROF_TAG_CPU_SAMPLES = 0x0D,
HPROF_TAG_CONTROL_SETTINGS = 0x0E,
};
需要重点关注的主要是三类信息:
- 字符串信息:保存着所有的字符串,在解析时可通过索引 id 引用
- 类的结构信息:包括类内部的变量布局,父类的信息等等
- 堆信息:内存占用与对象引用的详细信息
如果是堆信息,即 TAG 为 HEAP_DUMP 或 HEAP_DUMP_SEGMENT 时,那么其 BODY 由一系列子 record 组成,这些子 record 同样使用 TAG 来区分:
enum HprofHeapTag {
// Traditional.
HPROF_ROOT_UNKNOWN = 0xFF,
HPROF_ROOT_JNI_GLOBAL = 0x01, // native 变量
HPROF_ROOT_JNI_LOCAL = 0x02,
HPROF_ROOT_JAVA_FRAME = 0x03,
HPROF_ROOT_NATIVE_STACK = 0x04,
HPROF_ROOT_STICKY_CLASS = 0x05,
HPROF_ROOT_THREAD_BLOCK = 0x06,
HPROF_ROOT_MONITOR_USED = 0x07,
HPROF_ROOT_THREAD_OBJECT = 0x08,
HPROF_CLASS_DUMP = 0x20, // 类
HPROF_INSTANCE_DUMP = 0x21, // 实例对象
HPROF_OBJECT_ARRAY_DUMP = 0x22, // 对象数组
HPROF_PRIMITIVE_ARRAY_DUMP = 0x23, // 基础类型数组
// Android.
HPROF_HEAP_DUMP_INFO = 0xfe,
HPROF_ROOT_INTERNED_STRING = 0x89,
HPROF_ROOT_FINALIZING = 0x8a, // Obsolete.
HPROF_ROOT_DEBUGGER = 0x8b,
HPROF_ROOT_REFERENCE_CLEANUP = 0x8c, // Obsolete.
HPROF_ROOT_VM_INTERNAL = 0x8d,
HPROF_ROOT_JNI_MONITOR = 0x8e,
HPROF_UNREACHABLE = 0x90, // Obsolete.
HPROF_PRIMITIVE_ARRAY_NODATA_DUMP = 0xc3, // Obsolete.
};
每一个 TAG 及其对应的内容可参考 HPROF Agent,比如,String record 的格式如下:
因此,在读取 Hprof 文件时,如果 TAG 为 0x01,那么,当前 record 就是字符串,第一部分信息是字符串 ID,第二部分就是字符串的内容。
Hprof 文件裁剪
Matrix 的 Hprof 文件裁剪功能的目标是将 Bitmap 和 String 之外的所有对象的基础类型数组的值移除,因为 Hprof 文件的分析功能只需要用到字符串数组和 Bitmap 的 buffer 数组。另一方面,如果存在不同的 Bitmap 对象其 buffer 数组值相同的情况,则可以将它们指向同一个 buffer,以进一步减小文件尺寸。裁剪后的 Hprof 文件通常比源文件小 1/10 以上。
代码结构和 ASM 很像,主要由 HprofReader、HprofVisitor、HprofWriter 组成,分别对应 ASM 中的 ClassReader、ClassVisitor、ClassWriter。
HprofReader 用于读取 Hprof 文件中的数据,每读取到一种类型(使用 TAG 区分)的数据,就交给一系列 HprofVisitor 处理,最后由 HprofWriter 输出裁剪后的文件(HprofWriter 继承自 HprofVisitor)。
裁剪流程如下:
// 裁剪
public void shrink(File hprofIn, File hprofOut) throws IOException {
// 读取文件
final HprofReader reader = new HprofReader(new BufferedInputStream(is));
// 第一遍读取
reader.accept(new HprofInfoCollectVisitor());
// 第二遍读取
is.getChannel().position(0);
reader.accept(new HprofKeptBufferCollectVisitor());
// 第三遍读取,输出裁剪后的 Hprof 文件
is.getChannel().position(0);
reader.accept(new HprofBufferShrinkVisitor(new HprofWriter(os)));
}
可以看到,Matrix 为了完成裁剪功能,需要对输入的 hprof 文件重复读取三次,每次都由一个对应的 Visitor 处理。
读取 Hprof 文件
HprofReader 的源码很简单,先读取文件头,再读取 record,根据 TAG 区分 record 的类型,接着按照 HPROF Agent 给出的格式依次读取各种信息即可,读取完成后交给 HprofVisitor 处理。
读取文件头:
// 读取文件头
private void acceptHeader(HprofVisitor hv) throws IOException {
final String text = IOUtil.readNullTerminatedString(mStreamIn); // 连续读取数据,直到读取到 null
mIdSize = IOUtil.readBEInt(mStreamIn); // int 是 4 字节
final long timestamp = IOUtil.readBELong(mStreamIn); // long 是 8 字节
hv.visitHeader(text, idSize, timestamp); // 通知 Visitor
}
读取 record(以字符串为例):
// 读取文件内容
private void acceptRecord(HprofVisitor hv) throws IOException {
while (true) {
final int tag = mStreamIn.read(); // TAG 区分类型
final int timestamp = IOUtil.readBEInt(mStreamIn); // 时间戳
final long length = IOUtil.readBEInt(mStreamIn) & 0x00000000FFFFFFFFL; // Body 字节长
switch (tag) {
case HprofConstants.RECORD_TAG_STRING: // 字符串
acceptStringRecord(timestamp, length, hv);
break;
... // 其它类型
}
}
}
// 读取 String record
private void acceptStringRecord(int timestamp, long length, HprofVisitor hv) throws IOException {
final ID id = IOUtil.readID(mStreamIn, mIdSize); // IdSize 在读取文件头时确定
final String text = IOUtil.readString(mStreamIn, length - mIdSize); // Body 字节长减去 IdSize 剩下的就是字符串内容
hv.visitStringRecord(id, text, timestamp, length);
}
记录 Bitmap 和 String 类信息
为了完成上述裁剪目标,首先需要找到 Bitmap 及 String 类,及其内部的 mBuffer、value 字段,这也是裁剪流程中的第一个 Visitor 的作用:记录 Bitmap 和 String 类信息。
包括字符串 ID:
// 找到 Bitmap、String 类及其内部字段的字符串 ID
public void visitStringRecord(ID id, String text, int timestamp, long length) {
if (mBitmapClassNameStringId == null && "android.graphics.Bitmap".equals(text)) {
mBitmapClassNameStringId = id;
} else if (mMBufferFieldNameStringId == null && "mBuffer".equals(text)) {
mMBufferFieldNameStringId = id;
} else if (mMRecycledFieldNameStringId == null && "mRecycled".equals(text)) {
mMRecycledFieldNameStringId = id;
} else if (mStringClassNameStringId == null && "java.lang.String".equals(text)) {
mStringClassNameStringId = id;
} else if (mValueFieldNameStringId == null && "value".equals(text)) {
mValueFieldNameStringId = id;
}
}
Class ID:
// 找到 Bitmap 和 String 的 Class ID
public void visitLoadClassRecord(int serialNumber, ID classObjectId, int stackTraceSerial, ID classNameStringId, int timestamp, long length) {
if (mBmpClassId == null && mBitmapClassNameStringId != null && mBitmapClassNameStringId.equals(classNameStringId)) {
mBmpClassId = classObjectId;
} else if (mStringClassId == null && mStringClassNameStringId != null && mStringClassNameStringId.equals(classNameStringId)) {
mStringClassId = classObjectId;
}
}
以及它们拥有的字段:
// 记录 Bitmap 和 String 类的字段信息
public void visitHeapDumpClass(ID id, int stackSerialNumber, ID superClassId, ID classLoaderId, int instanceSize, Field[] staticFields, Field[] instanceFields) {
if (mBmpClassInstanceFields == null && mBmpClassId != null && mBmpClassId.equals(id)) {
mBmpClassInstanceFields = instanceFields;
} else if (mStringClassInstanceFields == null && mStringClassId != null && mStringClassId.equals(id)) {
mStringClassInstanceFields = instanceFields;
}
}
第二个 Visitor 用于记录所有 String 对象的 value ID:
// 如果是 String 对象,则添加其内部字段 "value" 的 ID
public void visitHeapDumpInstance(ID id, int stackId, ID typeId, byte[] instanceData) {
if (mStringClassId != null && mStringClassId.equals(typeId)) {
if (mValueFieldNameStringId.equals(fieldNameStringId)) {
strValueId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
}
mStringValueIds.add(strValueId);
}
}
以及 Bitmap 对象的 Buffer ID 与其对应的数组本身:
// 如果是 Bitmap 对象,则添加其内部字段 "mBuffer" 的 ID
public void visitHeapDumpInstance(ID id, int stackId, ID typeId, byte[] instanceData) {
if (mBmpClassId != null && mBmpClassId.equals(typeId)) {
if (mMBufferFieldNameStringId.equals(fieldNameStringId)) {
bufferId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
}
mBmpBufferIds.add(bufferId);
}
}
// 保存 Bitmap 对象的 mBuffer ID 及数组的映射关系
public void visitHeapDumpPrimitiveArray(int tag, ID id, int stackId, int numElements, int typeId, byte[] elements) {
mBufferIdToElementDataMap.put(id, elements);
}
接着分析所有 Bitmap 对象的 buffer 数组,如果其 MD5 相等,说明是同一张图片,就将这些重复的 buffer ID 映射起来,以便之后将它们指向同一个 buffer 数组,删除其它重复的数组:
final String buffMd5 = DigestUtil.getMD5String(elementData);
final ID mergedBufferId = duplicateBufferFilterMap.get(buffMd5); // 根据该 MD5 值对应的 buffer id
if (mergedBufferId == null) { // 如果 buffer id 为空,说明是一张新的图片
duplicateBufferFilterMap.put(buffMd5, bufferId);
} else { // 否则是相同的图片,将当前的 Bitmap buffer 指向之前保存的 buffer id,以便之后删除重复的图片数据
mBmpBufferIdToDeduplicatedIdMap.put(mergedBufferId, mergedBufferId);
mBmpBufferIdToDeduplicatedIdMap.put(bufferId, mergedBufferId);
}
裁剪 Hprof 文件数据
将上述数据收集完成之后,就可以输出裁剪后的文件了,裁剪后的 Hprof 文件的写入功能由 HprofWriter 完成,代码很简单,HprofReader 读取到数据之后就由 HprofWriter 原封不动地输出到新的文件即可,唯二需要注意的就是 Bitmap 和基础类型数组。
先看 Bitmap,在输出 Bitmap 对象时,需要将相同的 Bitmap 数组指向同一个 buffer ID,以便接下来剔除重复的 buffer 数据:
// 将相同的 Bitmap 数组指向同一个 buffer ID
public void visitHeapDumpInstance(ID id, int stackId, ID typeId, byte[] instanceData) {
if (typeId.equals(mBmpClassId)) {
ID bufferId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
// 找到共同的 buffer id
final ID deduplicatedId = mBmpBufferIdToDeduplicatedIdMap.get(bufferId);
if (deduplicatedId != null && !bufferId.equals(deduplicatedId) && !bufferId.equals(mNullBufferId)) {
modifyIdInBuffer(instanceData, bufferIdPos, deduplicatedId);
}
// 修改完毕后再写入到新文件中
super.visitHeapDumpInstance(id, stackId, typeId, instanceData);
}
// 修改成对应的 buffer id
private void modifyIdInBuffer(byte[] buf, int off, ID newId) {
final ByteBuffer bBuf = ByteBuffer.wrap(buf);
bBuf.position(off);
bBuf.put(newId.getBytes());
}
}
对于基础类型数组,如果不是 Bitmap 中的 mBuffer 字段或者 String 中的 value 字段,则不写入到新文件中:
public void visitHeapDumpPrimitiveArray(int tag, ID id, int stackId, int numElements, int typeId, byte[] elements) {
final ID deduplicatedID = mBmpBufferIdToDeduplicatedIdMap.get(id);
// 如果既不是 Bitmap 中的 mBuffer 字段, 也不是 String 中的 value 字段,则舍弃该数据
// 如果当前 id 不等于 deduplicatedID,说明这是另一张重复的图片,它的图像数据不需要重复输出
if (!id.equals(deduplicatedID) && !mStringValueIds.contains(id)) {
return; // 直接返回,不写入新文件中
}
super.visitHeapDumpPrimitiveArray(tag, id, stackId, numElements, typeId, elements);
}
总结
Hprof 文件格式
Hprof 文件由文件头和文件内容两部分组成,文件内容由一系列 records 组成,record 的类型则通过 TAG 来区分。
Hprof 文件格式示意图:
文件头:
record:
其中文件内容需要关注的主要是三类信息:
- 字符串信息:保存着所有的字符串,在解析时可通过索引 id 引用
- 类的结构信息:包括类内部的变量布局,父类的信息等等
- 堆信息:内存占用与对象引用的详细信息
更详细的格式可参考文档 HPROF Agent。
Hprof 文件裁剪
Matrix 的 Hprof 文件裁剪功能的目标是将 Bitmap 和 String 之外的所有对象的基础类型数组的值移除,因为 Hprof 文件的分析功能只需要用到字符串数组和 Bitmap 的 buffer 数组。另一方面,如果存在不同的 Bitmap 对象其 buffer 数组值相同的情况,则可以将它们指向同一个 buffer,以进一步减小文件尺寸。裁剪后的 Hprof 文件通常比源文件小 1/10 以上。
Hprof 文件裁剪功能的代码结构和 ASM 很像,主要由 HprofReader、HprofVisitor、HprofWriter 组成,HprofReader 用于读取 Hprof 文件中的数据,每读取到一种类型(使用 TAG 区分)的数据(即 record),就交给一系列 HprofVisitor 处理,最后由 HprofWriter 输出裁剪后的文件(HprofWriter 继承自 HprofVisitor)。
裁剪流程如下:
- 读取 Hprof 文件
- 记录 Bitmap 和 String 类信息
- 移除 Bitmap buffer 和 String value 之外的基础类型数组
- 将同一张图片的 Bitmap buffer 指向同一个 buffer id,移除重复的 Bitmap buffer
- 其它数据原封不动地输出到新文件中
需要注意的是,Bitmap 的 mBuffer 字段在 API 26 被移除了,因此 Matrix 无法分析 API 26 以上的设备的重复 Bitmap。