Hprof 文件使用的基本数据类型为:u一、u二、u四、u8,分别表示 1 byte、2 byte、4 byte、8 byte 的内容,由文件头和文件内容两部分组成。html
其中,文件头包含如下信息:java
长度 | 含义 |
---|---|
[u1]* | 以 null 结尾的一串字节,用于表示格式名称及版本,好比 JAVA PROFILE 1.0.1(由 18 个 u1 字节组成) |
u4 | size of identifiers,即字符串、对象、堆栈等信息的 id 的长度(不少 record 的具体信息须要经过 id 来查找) |
u8 | 时间戳,时间戳,1970/1/1 以来的毫秒数 |
文件内容由一系列 records 组成,每个 record 包含以下信息:android
长度 | 含义 |
---|---|
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, }; 复制代码
须要重点关注的主要是三类信息:markdown
若是是堆信息,即 TAG 为 HEAP_DUMP 或 HEAP_DUMP_SEGMENT 时,那么其 BODY 由一系列子 record 组成,这些子 record 一样使用 TAG 来区分:jvm
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 的格式以下:ide
所以,在读取 Hprof 文件时,若是 TAG 为 0x01,那么,当前 record 就是字符串,第一部分信息是字符串 ID,第二部分就是字符串的内容。oop
Matrix 的 Hprof 文件裁剪功能的目标是将 Bitmap 和 String 以外的全部对象的基础类型数组的值移除,由于 Hprof 文件的分析功能只须要用到字符串数组和 Bitmap 的 buffer 数组。另外一方面,若是存在不一样的 Bitmap 对象其 buffer 数组值相同的状况,则能够将它们指向同一个 buffer,以进一步减少文件尺寸。裁剪后的 Hprof 文件一般比源文件小 1/10 以上。布局
代码结构和 ASM 很像,主要由 HprofReader、HprofVisitor、HprofWriter 组成,分别对应 ASM 中的 ClassReader、ClassVisitor、ClassWriter。spa
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 处理。
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 类,及其内部的 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 文件的写入功能由 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 文件由文件头和文件内容两部分组成,文件内容由一系列 records 组成,record 的类型则经过 TAG 来区分。
Hprof 文件格式示意图:
文件头:
record:
其中文件内容须要关注的主要是三类信息:
更详细的格式可参考文档 HPROF Agent。
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)。
裁剪流程以下:
须要注意的是,Bitmap 的 mBuffer 字段在 API 26 被移除了,所以 Matrix 没法分析 API 26 以上的设备的重复 Bitmap。