1. 程式人生 > >Unity3D asset bundle 格式解析

Unity3D asset bundle 格式解析

Unity3D asset bundle 格式簡析

Unity3D 的 asset bundle 的格式並沒有公開。但為了做更好的差異更新,我們還是希望瞭解其打包格式。這樣可以製作專門的差異比較合併工具,會比直接做二進位制差異比較效果好的多。因為可以把 asset bundle 內的資料拆分為獨立單元,只對變更的單元做差異比較即可。

網上能查到的資料並不是官方給出的,最為流行的是一個叫做 disunity 的開源工具。它是用 java 編寫的,只有原始碼,而沒有給出格式說明(而後者比程式碼重要的多)。通過閱讀 disunity 的程式碼,我整理出如下記錄:

asset bundle 分為壓縮模式和非壓縮模式。壓縮模式僅僅是用開源的 lzma 庫 對整個非壓縮包做了一次整體壓縮。壓縮資料的頭有 13 個位元組,前 5 個位元組是 lzma 解壓縮的 API 需要穿入的 props ,接下來的 4 位元組是解壓縮後的資料庫長度。最後 4 位元組不用理會它。

把壓縮資料解開後,就和非壓縮模式沒有差別,下面只討論非壓縮格式:

assert bundle 的檔案頭是從這樣一個數據結構序列化出來的。

struct AssetBundleFileHead {
struct LevelInfo {
unsigned int PackSize;
unsigned int UncompressedSize;
};

 string          FileID;
 unsigned int     Version;
 string          MainVersion;
 string          BuildVersion;
 size_t          MinimumStreamedBytes;
 size_t          HeaderSize;
 size_t          NumberOfLevelsToDownloadBeforeStreaming;
 size_t          LevelCount;
 LevelInfo     LevelList[];
 size_t          CompleteFileSize;
 size_t          FileInfoHeaderSize;
 bool          Compressed;

};
string 是直接以 \0 結尾的字串,順序序列化;size_t 是大端的 4 位元組數字;bool 是單個位元組;vector 就是順著排列的結構。

根據 Unity 版本的不同,assert bundle 的格式也不完全相同。Version 指明瞭 bundle 的格式版本,從 Unity 3.5 開始到 4.x 版都使用 Version = 3 ,下面只討論這個版本。HeaderSize 應該恰好等於以上這個檔案頭的資料長度。

一個 assert bundle 是由多個 asset 檔案打包而成,接下來順序打包了這些 asset 。序列化成這樣的結構:

struct AssetFileHeader {
struct AssetFileInfo {
string name;
size_t offset;
size_t length;
};
size_t FileCount;
AssetFileInfo File[];
};
這樣我們就可以分解出被打包在一起的多個 Asset 了(大多數情況下只有一個)。offset 表示的是除去 HeaderSize 後的偏移量。我們可以用 HeaderSize 加上那個部分的 offset 得到這個部分相對於整個 bundle 的檔案偏移。

對於每個 asset ,又有它自己的資料頭。資料頭除了基本的資料頭結構 AssetHeader 外,還有額外的三個部分。disunity 把它們稱為 TypeTree ObjectPath 和 AssetRef 。注意:這裡 Format 隨不同 Unity3D 的版本有所不同,我們只關心目前的版本的格式,這裡 Format 為 9 (其它版本的格式,在大小端等問題上有所不同)。

struct AssetHeader {
size_t TypeTreeSize;
size_t FileSize;
unsigned int Format;
size_t dataOffset;
size_t Unknown;
Unity 對 Asset 資料做了簡單粗暴的序列化操作。整個序列化過程是針對每種物件的資料結構進行的。TypeTree 是對資料結構本身的描述,通過這個描述,就可以反序列化出每個物件。

AssetHeader 後面緊跟著的就是 TypeTree 。但是,這個 TypeTree 對於 asset bundle 來說是可選的,因為資料結構的資訊可以事先放置在引擎中(引擎多半隻支援固有的資料型別)。在釋出到移動裝置上時,TypeTree 是不打包到 asset bundle 中的。

每個 asset 物件,都有一個 class id ,可以在 TypeTree 中查到如何反序列化。class id 的和具體型別的對應關係,在 Unity3d 的官方文件 可以查到。但若我們只是想將差異比較在物件一級進行(而不是具體比較物件中具體的屬性),那麼就不需要解開具體物件的細節資訊,這部分也不用關心。所以這裡也不展開(有興趣可以讀一下 disunity 的程式碼,格式並不複雜)。

在 AssetHeader 中的 TypeTreeSize 指的就是 TypeTree 部分的大小。接下來是每個 AssetObject 的描述資料。

struct ObjectHeader {
struct ObjectInfo {
int pathID;
int offset;
int length;
byte classID[8];
};
int ObjectCount;
ObjectInfo Object[];
};
這裡,所有的 int 都是以小端編碼的 4 位元組整數(不同於外部檔案格式採用的大端編碼)。在 Unity3D 中,每個物件都有唯一的字串 path ,但是在 asset bundle 裡並沒有直接儲存字串,而是一個 hash 過的整數,也可以看成是對這個物件的索引號。真正的物件放在資料頭的後面,偏移量為 offset 的地方。

這裡的 offset 是相對當前 asset 塊的。如果想取得正確的相對整個檔案的位置,應該是檔案的 HeaderSize + asset 的 offset + asset 的 dataOffset + 這裡的 object offset 。

接在 ObjectHeader 後的是 AssetRef 表,記錄了 Asset 的引用關係。用於指明這個 bundle 內 asset 對外部 asset 的引用情況。AssetRefTable 結構如下:

struct AssetTable {
struct AssetRef {
byte GUID[8];
int type;
string filePath;
string assetPath;
};
int Count;
byte Unknown;
vector Refs;