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

Hprof 文件格式

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

  1. 字符串信息:保存着全部的字符串,在解析時可經過索引 id 引用
  2. 類的結構信息:包括類內部的變量佈局,父類的信息等等
  3. 堆信息:內存佔用與對象引用的詳細信息

若是是堆信息,即 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

string record

所以,在讀取 Hprof 文件時,若是 TAG 爲 0x01,那麼,當前 record 就是字符串,第一部分信息是字符串 ID,第二部分就是字符串的內容。oop

Hprof 文件裁剪

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 處理。

讀取 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。

相關文章
相關標籤/搜索