Android 性能监控框架 Matrix(3)Hprof 文件分析

3,932 阅读8分钟

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,
};

需要重点关注的主要是三类信息:

  1. 字符串信息:保存着所有的字符串,在解析时可通过索引 id 引用
  2. 类的结构信息:包括类内部的变量布局,父类的信息等等
  3. 堆信息:内存占用与对象引用的详细信息

如果是堆信息,即 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 的格式如下:

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 文件格式示意图:

Hprof 文件

文件头:

Hprof 文件头

record:

Hprof record

其中文件内容需要关注的主要是三类信息:

  1. 字符串信息:保存着所有的字符串,在解析时可通过索引 id 引用
  2. 类的结构信息:包括类内部的变量布局,父类的信息等等
  3. 堆信息:内存占用与对象引用的详细信息

更详细的格式可参考文档 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)。

裁剪流程如下:

  1. 读取 Hprof 文件
  2. 记录 Bitmap 和 String 类信息
  3. 移除 Bitmap buffer 和 String value 之外的基础类型数组
  4. 将同一张图片的 Bitmap buffer 指向同一个 buffer id,移除重复的 Bitmap buffer
  5. 其它数据原封不动地输出到新文件中

需要注意的是,Bitmap 的 mBuffer 字段在 API 26 被移除了,因此 Matrix 无法分析 API 26 以上的设备的重复 Bitmap。