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;

COMMENTS

BuildAssetBundleOptions.DeterministicAssetBundle 用這個參數打包path_id時hash 否則是隻增長數

struct ObjectInfo {
int offset;
int length;
int pathID;
byte classID[8];
};
pathId應該在length後面

如約而至:https://github.com/dpull/AssetBundlePatch

我在寫一個差異更新assetbundle的工具,寫完後我會開源出來,目前進度是做兩個assetbundle包的對比,發現了幾個問題:

1、byte GUID[8];應當是byte GUID[16];
2、path_id並不是hash

每次同樣的asset,打包後的包大小都不一致 是因爲生成assetbundle的時候用了 lzma壓縮,只要壓縮前的數據有一點點不一樣,那lzma 後的結果就 幾乎完全不一樣。
想要得到一模一樣的assetbundle包,要在BuildAssetBundle的assetBundleOption裏 |BuildAssetBundleOptions.DeterministicAssetBundle 來確保文件的唯一性。

感謝disunity作者的辛勤工作,unity資源文件有它固有的格式,且unity不是開源引擎,研究摸索它的格式需要一定的耐心。

這個很有用!我也要做自定義的Unity的資源加載和更新系統。

回復Lazy ,我試過在本機Windows打包MD5驗證時一樣的,換了一臺機器就不一樣了,估計把機器UUID也放了進去吧

可以解釋下爲什麼每次同樣的asset,打包後的包大小都不一致麼?從你解釋的結構來看好像沒有什麼不穩定的數據啊。

期待雲風給出patch方案:)

向請教您一個問題,如果卡牌遊戲中數玩家的卡牌過多,存儲在數據庫中的卡牌數量就很大,玩家登錄讀取數據時,這部分的數據讀取是最慢的,雖然做了分表操作。有什麼比較好的解決方案嗎?

打算自己做patch麼?狂刃的二進制patch工具還不錯啊,好像是64k一個塊,只要打包的時候是排過序的,最終生成的差異還是比較小的。
這個未公開的結構有可能隨時變化吧。不如跟unity聯繫讓他們做一個patch工具好了。

那13字節是標準的LZMA壓縮文件頭, 後8個字節是long類型小端表示的長度.
7-zip可以直接打開這種以.lzma後綴的壓縮文件.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章