一個靈活的AssetBundle打包工具
![技術分享圖片](http://upload-images.jianshu.io/upload_images/3503018-bff134fddaf74146.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/584)
上周介紹了Unity項目中的資源配置,今天和大家分享一個AssetBundle打包工具。相信從事Unity開發或多或少都了解過AssetBundle,但簡單的接口以及眾多的細碎問題也給工作帶來較多的困擾。今天分享AssetBundle工具的實踐與想法,相信這塊內容對幫助理解AssetBundle有較大的幫助。
Unity提供了兩種資源加載方式,一種是Resources,另外種就是AssetBundle。所有的資源只要放在Resources目錄下,在打包的時候會自動打進去,並可以通過相應的接口加載。正常情況下Resources非常方便,可以滿足日常的需求,但資源放Resources會帶來資源更新上的問題。之前寫過一篇文章Unity資源目錄及加載接口介紹可以了解些細節。
假設首包所有資源都放Resources,後續更新資源的走AssetBundle,會發現AssetBundle和Resources的資源互相不兼容。當調整一個模型的材質參數後,對模型進行打包仍需要把Mesh,Texture等資源都打進去。這會導致更新包過大,同時在加載這個模型時,這些資源是不共用的,相同的資源可能在內存中存在兩份。所以正常情況下,項目發布時所有需要更新的資源要打成AssetBundle。
正常項目中資源的提交與變更非常頻繁,手工對每個資源配置Bundle費時費力,基本不可取。所以一般項目中的Bundle都是程序自動創建的。同時為了避免有多余的資源被打包,通常需要配置哪些資源是發布資源(直接加載的),其他資源通過引用的形式獲取。這個配置需要方便修改,來滿足日常變更。
Bundle的打包規則對資源加載速度,更新大小,重復資源數量以及最終包數量等等都有較大影響。一個可靠的Bundle打包方案應該是根據實際情況對Bundle打包規則做調整慢慢產生的。
在Unity 4,只有最基礎的幾個打包接口可以用於打包。Unity 5簡化了Bundle打包時候的依賴關系,但實際如何創建Bundle以及對依賴資源的配置都節省不了。遠遠不能滿足項目對資源打包這塊的需求。
這裏實現的AssetBundle打包工具幫助簡化這個繁瑣的打包過程,同時方便做規則調整,得到更優的打包方案。目前工具BundleBuildTool已經放在GitHub,可以作為一份打包實現的參考,也可以直接使用這工具來進行打包。
AssetBundle
An AssetBundle is an archive file containing platform specific Assets (Models, Textures, Prefabs, Audio clips, and even entire Scenes) that can be loaded at runtime.
資源類型
不同類型資源會有不同的打包方式,比如場景文件的打包接口和其他資源的打包接口就是不一樣的。通過定義不同的資源類型,可以實現不同的打包方式,支持更多資源的打包。
public enum BundleType
{
None = 0,
Script, // .cs
Shader, // .shader or build-in shader with name
Font, // .ttf
Texture, // .tga, .png, .jpg, .tif, .psd, .exr
Material, // .mat
Animation, // .anim
Controller, // .controller
FBX, // .fbx
TextAsset, // .txt, .bytes
Prefab, // .prefab
UnityMap, // .unity
}
對於特殊類型的資源,通過類型可以做一些定制化操作。比如把所有的Script配置在一個Bundle裏面,然後在啟動的時候對這個Bundle做預加載。通常情況下也會把所有的Shader配置到一個Bundle裏面。
正常一個模型會有自己的Texture,Mesh & Animation,把資源按類型打成三個包,在加載的時候可以得到更高的加載速度。Unity異步加載接口會同時進行多個資源加載,資源配置在不同的包裏,可以有較好的加載速度提升,所以一般是按資源類型來進行打包。不過要註意如果太分散的話,一樣會影響加載速度。
資源加載速度這個是在文章Asset Bundles vs. Resources: A Memory Showdown提及。
These blocks sizes are optimized for loading multiple Assets and bundles in parallel. For example, you should be able to load objects from 4 to 5 Asset Bundles at the same time without the the allocators for Asset Bundle Async loading or Type Trees needing new blocks.
資源依賴
處理資源依賴應該是打包過程最復雜的一塊功能,這裏把獲取資源依賴文件列表單獨設計一個類,做一些特殊情況處理。如果發現一些依賴關系上的錯誤,除了修改資源本身外,也可以在打包環節實現一些腳步做保障。
正常情況下,通過AssetDatabase.GetDependencies即可獲取一個資源的所以依賴文件。但實際情況中,Unity內部是通過分析內部guid來生成依賴文件。有時候在文件裏面會存在一些臟的guid這會產生多余的依賴。比如你修改一個材質貼圖屬性名,然後設置了一張新的貼圖給這個新的屬性名。打開材質文件會發現舊的屬性名以及引用guid出現在材質文件,通過GetDependencies獲取的最後結果也包含這個數據。實現自己獲取依賴函數來處理這種多余依賴關系。同時提供帶緩存接口,提高打包效率。
下面是對材質依賴貼圖文件獲取的代碼實現。
...
MaterialProperty[] proTes = MaterialEditor.GetMaterialProperties(new Object[] {mat});
for (int i = 0; proTes != null && i < proTes.Length; ++i)
{
if (proTes[i].type == MaterialProperty.PropType.Texture)
{
Texture tex = mat.GetTexture(proTes[i].name);
string path = AssetDatabase.GetAssetPath(tex);
if (!dict.ContainsKey(path))
{
dict.Add(path, path);
}
Resources.UnloadAsset(tex);
}
}
...
資源剔除
處理完資源依賴後,還碰到一個問題就是最後打包Assets資源。通過AssetDatabase.LoadAllAssetAtPath獲取這個文件依賴的所有的Assets資源。如果對所有的這些Assets資源都做打包的話,會發現一些編輯器用數據也會被打包進去。特別是對於FBX類型文件,通常會存在一個"__preview_Take 001"的動作資源使包體變大很多。對於這些不必要的數據,在打包環節中增加一個剔除規則,減少包體大小。
public static List<UnityEngine.Object> FilterObjectByType(UnityEngine.Object[] assets, BundleType bundleType)
{
List<UnityEngine.Object> ret = new List<UnityEngine.Object>();
foreach (UnityEngine.Object asset in assets)
{
switch (bundleType)
{
case BundleType.FBX:
if (!(asset.GetType() == typeof(AnimationClip) && asset.name == "__preview_Take 001"))
{
ret.Add(asset);
}
break;
default:
ret.Add(asset);
break;
}
}
return ret;
}
Unity 5剛出的時候會把這個數據打進AssetBundle造成包體過大,後面版本觀察已經修復這個問題。不過也可以發現這個環節的必要性,如果發現資源出問題在這個環節處理即可。
這個環節不僅可以剔除不必要的數據,還可以直接修改數據本身。就拿Mesh數據舉例,美術在制作過程中會導出多余的頂點數據在文件裏面(uv3,uv4...)。通常配置Optimize Mesh可以幹掉這些無用數據,不過直接啟用可能會出現刪除了需要數據情況,比如color數據丟失。所以自己來做,通過把Mesh對象上不需要的對象數據置空,然後再打包即可。在之前分享的資源配置工具裏已經做了對Mesh頂點數據的配置,基本上就是為這個打包環節服務,因為無法修改FBX文件,只能美術重新導出。
資源大小
資源大小影響最後的包體大小,如果對包體大小以及更新量有關註的話,對資源大小做預估是一個非常有必要的環節。在資源大小計算環節,不能疏漏之前二個資源環節對資源的處理,同時不同類型的資源統計方式不一樣。
通常通過下面兩個方式預估資源大小
int resSize = UnityEngine.Profiling.Profiler.GetRuntimeMemorySize(asset);
FileInfo fileInfo = new FileInfo(assetPath);
int fileSize = fileInfo.Length;
如何對一個資源做一個大小估算,並不是一件非常方便的事情的。如果依賴資源已經在之前打包了,那這個資源的實際大小是要考慮減去依賴資源那部分的大小。如果不統計依賴資源的大小,那這個資源的包的大小也是不準確的。所以這裏的實際邏輯較為復雜,但實際一個大致的值就可以了,然後觀察最後的包大小做一些配置微調即可。
Bundle模型
討論完資源上的一些細節,下面開始Bundle設計的介紹。一個Bundle模型用name做唯一標識,為了方便管理加入了parent與children數據。同時一個Bundle應該有一個固定資源類型。為了方便對包大小做限制加入了size屬性,作為資源大小的預估。
public class BundleData
{
public string name = string.Empty;
public string parent = string.Empty;
public BundleType type = BundleType.None;
public BundleLoadState loadState = BundleLoadState.UnloadImmediately;
public int size = 0;
public List<string> includs = new List<string>();
public List<string> children = new List<string>();
}
最後一個Bundle包含多個資源文件路徑。盡管AssetBundle是按Assets打包的,但在正常環境下的資源是以文件存在的。一個資源文件可能包含多個資源,也可能引用到其他資源。資源文件可以用路徑來標識,Unity內部通過GUID來標識資源文件,所以即使你挪動文件因為GUID不變,還是可以找到這個文件。這裏決定直接用資源路徑來標識資源而不是使用GUID,因為挪動資源目錄有較多的風險,原則上禁止挪動資源。如果真挪動了資源,按最新的資源路徑生成Bundle是一個不錯的選擇。
如果有對Bundle有其他屬性上的需求,在這個類擴展就好。
Bundle創建規則
定義Bundle後,創建Bundle是很困擾的一個問題。在大型項目中,資源的量非常大,資源之間的互相引用也較為復雜。這裏定義一個數據結構幫忙創建Bundle。
public class BundleImportData
{
public string RootPath = "";
public string FileNameMatch = "*.*";
public int Index = -1;
public int TotalCount = 0;
public BundleType Type = BundleType.None;
public BundleLoadState LoadState = BundleLoadState.OnUnloadAsset;
public bool Publish = false;
public int LimitCount = -1;
public int LimitKBSize = -1;
public bool PushDependice = false;
public bool SkipData = false;
}
對於一個Bundle,可以約束它的大小,對象數量、類型、加載方式、打包方式。然後根據規則,自動給每個資源文件配置Bundle。
資源分加載資源和被依賴引用到的資源,對於直接加載的資源,需要配置Publish為True。Bundle創建就是從這些配置了Publish的資源文件以及其依賴生成的。
對所有可能被打包的資源配置打包規則,沒有被配置資源文件,則會被一起打倒最後資源的包裏面。這裏會碰到一個問題,有些資源需要補分包,但是通用規則會包含不需要分包的資源。這裏增加了一個SkipData屬性,當為True時這些資源不單獨創建Bundle。
然後討論下PushDependice屬性,正常情況下只有在打Prefab類型的資源的時候才會做這個操作。因為Prefab數據本身是不共享的,然後避免Prefab與Prefab之間的復雜依賴。
最後討論下打包的順序,因為資源之間有互相依賴,所以需要配置資源的打包順序。這裏資源的打包順序就是BundleImportData創建的順序。這裏需要對資源之間的依賴以及資源類型有一定的認識。
已經配置過Bundle的資源不會變更,新增的資源會按規則配置相應的Bundle。通常規則發生變更會影響非常多的資源,如果所有資源重新配置會導致更新包過大。
Bundle構建
首次創建的Bundle,由於本地文件不存在,會觸發構建。然後資源之間有互相依賴,所有被依賴的Bundle也需要參加構建。對於增量構建,這裏做了一個簡化設計,不自己去計算文件是否變更,而是由外部提供一個文件變化列表。通過這個列表工具自動生成Bundle構建列表,提高打包速度。
在配置打包參數為BuildAssetBundleOptions.DeterministicAssetBundle後,如果不對資源做修改,兩次打包的文件是一樣的。所以即使有很多資源因為依賴要重新打包,最後的文件未發生變化,就不會觸發更新。
Bundle索引
Bundle構建完後只是一堆二進制文件,需要根據Bundle之間的依賴關系生成出一份數據。除了需要知道Bundle之間的依賴之外,同時還需要知道資源路徑與Bundle之間的映射關系。最後還要把Bundle狀態信息保存下來,用於Bundle更新、加載和卸載。
public class BundleState
{
public string bundleID = string.Empty;
public uint crc = 0;
public uint compressCrc = 0;
public int version = -1;
public long size = -1;
public BundleLoadState loadState = BundleLoadState.OnUnloadAsset;
public BundleStorePos storePos = BundleStorePos.Building;
}
// like UnityEngine.AssetBundleManifest
public class BundleManifest { ... }
這個文件自己定義形式,可以使分散的多個文件,也可以統一放到一個文件裏面,自己實現可以優化數據結構減少內存開銷。
通用的Bundle打包方案
下面是在Unity Standard Assets資源上做配置後的結果
![技術分享圖片](http://upload-images.jianshu.io/upload_images/3503018-35ae7aca0795e1d6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
按大小配置基礎資源,然後對於Prefab和Unity文件限定下個數,避免過多的資源依賴。配置結束後點擊CreateBundle就可以得到下面的結果。
[完 2017-07-13 Carber]
- AssetBundle打包工具
- 本文首發於我的簡書博客(鏈接)
- 本文同時發布在知乎專欄(鏈接)
作者:carber
鏈接:https://www.jianshu.com/p/a7720cbbe4c4
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並註明出處。
一個靈活的AssetBundle打包工具