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。