Unity 5.x AssetBundle零冗餘解決方案

Unity 5.x AssetBundle零冗餘解決方案

https://blog.uwa4d.com/archives/1577.html

 

最近筆者參考Unity官方介紹資源管理機制和Unity序列化的一些文章做了下AssetBundle打包相關的優化,成功實現了零冗餘打包,下面和大家分享一下實現的過程。


一、問題概述

相比Unity 4.x版本里AssetBundle(以下簡稱ab)打包複雜的API,Unity 5.x版本里提供了更加人性化的依賴自動管理機制——對指定打包的資源,Unity會自動收集並分析其依賴的資源,如果該資源依賴的某個資源沒有被顯式指定打包到ab中,就將其依賴的這個資源打包進該資源所在的ab裏;如果已經被指定打包進其他ab裏,那麼這兩個ab之間就會構成依賴關係,加載ab時,先加載其依賴的ab即可。

這一套依賴管理機制使用方便的同時也會帶來一個問題:如果兩個ab A和B中的一些資源都依賴了一個沒有被指定要打包的資源C,那麼C就會同時被打進ab A和B中,造成資源的冗餘,增大ab和安裝包的體積。而這個被A,B依賴的資源C又可以分爲兩種類型,一種是Assets下外部導入的資源,即開發者導入或創建的資源;另一種則是Unity內置的資源,例如內置的Shader,Default-Material和UGUI一些組件如Image用的一些紋理資源等等。因此要解決資源冗餘的問題,就要分別對這兩種被依賴的資源進行處理。下圖是我們的一個項目優化前使用UWA提供的資源檢測工具檢測到的資源冗餘情況,可以發現問題較爲嚴重:

請輸入圖片描述


二、處理被依賴的外部資源

對於沒有被指定打包的外部資源,如果多個ab包依賴了它,打包時該資源就會被多次打包進依賴它的ab包中,造成冗餘。解決方案就是將這些被多個ab包依賴的資源打包到一個公共ab包中。處理過程如下:

  1. 使用EditorUtility.CollectDependencies()得到ab依賴的所有資源的路徑;
  2. 統計資源被所有ab引用的次數,將被多個ab引用的資源打包爲公共ab包。

需要注意的地方:
EditorUtility.CollectDependencies()收集到的資源包含了腳本,dll和編輯器資源,這些資源無需打進ab中。

更詳細的步驟可以參考這篇文章:如何優化AssetBundle資源

至此,再次進行檢測我們可以發現冗餘的資源數大大減少,冗餘的外部資源已經被消滅,只剩下一些冗餘的Unity內置資源,接下來我們再來處理這些冗餘的內置資源。
請輸入圖片描述


三、處理被依賴的內部資源

經過上面的處理,ab中爲什麼還會有冗餘的內置資源?我們來看上面獲得ab依賴資源的過程,對於外部資源,可以使用AssetDataBase.GetAssetPath()獲得該資源在Assets下的路徑;但是對於內置資源,獲取的路徑卻始終是"Resources/unity_builtin_extra"(後面會提到另外一種情況),unity_builtin_extra文件在Assets下並不可見,但會出現在安裝包Data/Resources下,可以猜測unity_builtin_extra是Unity內部使用的一個資源庫,類似於ab,但無法像對待外置資源那樣對其進行打包。因此解決問題的關鍵就是修改這些資源對內部資源的引用,讓其引用外部的資源。

第一種方案是不使用內置資源,該方案理論上可行實際上幾乎不可行,例如一些Shader的FallBack是Leagcy VertexLit,使用了該Shader的資源會依賴Leagcy VertexLit(Shader也會依賴FallBack Shader);Unity自帶的3D Object默認使用了Default-Material材質,粒子系統則默認使用了Default-Particle,UGUI一些則組件會用到Background這些內置Sprite等等。

第二種方案則是提取出Unity內置的資源,在打ab前進行預處理,修改引用了內置資源的資源使其引用提取出的那一份資源,接着按處理外部資源的流程進行打包,這樣被多次引用的“內置資源”(這裏指的是提取出的那一份內置資源)可以被正確的打入ab包,打包完成後再將之前步驟修改過的引用還原即可。這樣即不影響項目的正常開發,也從根本上避免了內置資源的冗餘。該方案地實現步驟如下:

1. 提取出Unity內置的資源
使用AssetDataBase.LoadAllAssetsAtPath() 可以加載出"Resources/unity_builtin_extra"下所有的Object,可以發現共有4種類型的資源:Shader,Material,Texture以及Sprite。對於內置Shader,我們可以直接從Unity官方網站下載;而對於後三種,可以通過AssetDataBase提供的相關API來進行創建:

Object[] UnityAssets = AssetDatabase.LoadAllAssetsAtPath("Resources/unity_builtin_extra");
foreach (var asset in UnityAssets)
{
   // create asset...
}

提取出內置資源之後,我們的做法是將shader和其他資源做成兩個壓縮包放在項目Editor下,在打ab前解壓出來隨後進行相關的修改,最後再刪除解壓出來的內置資源。

2. 修改相關資源的引用(難點)

首先我們將Unity資源分爲兩種類型:

基本類型: Mesh,Texture,AudioClip,Anim等不會引用其他資源的類型
複合類型: 能引用其他資源的類型,有4種:.unity,.prefab,.material,.asset。

參考Unity Manual上介紹Unity序列化的章節,我們可以瞭解到複合類型的資源本質上都是使用YAML這種標記語言編寫的序列化文件,用於描述Unity中組件的各種關係,也包含對依賴關係的描述。將EditorSettings下的Asset Serialization Mode設置爲Force Text後,在操作系統的資源管理器下可以以文本文件的形式查看這類文件的內容,示例爲Default-Material.mat文件的內容:

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
  serializedVersion: 6
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  m_Name: Default-Diffuse
  m_Shader: {fileID: 7, guid: 0000000000000000f000000000000000, type: 0}

結合EditorUtility.CollectDependencies()對比分析,可以發現以下格式描述了一條對其他資源的引用:

{fileID: xxxxxx, guid: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, type: x}

fileId和guid標識了是被引用的資源,而type則表示被引用資源的類型。通過進一步的實驗推測出0代表Unity內置資源,2代表複合類型的外部資源,3代表了基礎類型的外部資源以及Missing Reference的情況,1的含義未知。

在Unity中,每個資源導入或創建時都會被分配一個唯一的guid和fileID,guid可以通過AssetDataBase.AssetPathToGUID()獲得,而Unity並沒有提供獲得file ID的API,只能將Inspector面版設置爲Debug模式看到資源的file ID。不過我們依然可以通過反射配合SerializedObject取得file ID的值:

private static PropertyInfo inspectorMode = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
public static long GetFileID(this Object target)
{
    SerializedObject serializedObject = new SerializedObject(target);
    inspectorMode.SetValue(serializedObject, InspectorMode.Debug, null);
    SerializedProperty localIdProp = serializedObject.FindProperty("m_LocalIdentfierInFile");
    return localIdProp.longValue;
}

在以上分析的基礎上,我們可以實現以下兩個功能:

  1. 獲得任意資源(包含內置)的file ID和guid,並能根據file ID和guid反向得到對應的資源。
  2. 直接讀寫YAML文件(只有複合類型纔是YAML文件)實現修改複合類型資源對其他資源的引用。

最後利用以上兩個功能來實現替換Unity內置資源,步驟如下:

第一步:解壓提取出的內置資源,建立起提取出的內置資源於內置資源的MetaData(封裝了guid和file ID)的映射關係。

第二步:進入處理外置資源的流程,增加一步對複合類型資源及其依賴的複合類型資源是否引用內置資源的檢測,如果發現引用則讀取對應的YAML文件修改fileID和guid進行替換,並將修改記錄下來以便還原。

第三步:等待打包完畢,恢復各項對資源引用的修改,清理解壓出來的內置文件。

至此,我們再進行資源冗餘檢測,可以發現內置資源的冗餘也減少了很多,但是還是有個別的殘留:

請輸入圖片描述

進一步的探索後,發現Unity具有兩種內置資源,一套是"Resources/unity_builtin_extra",主要包含了是Shader,Material,UGUI的Sprite和Sprite對應的Texture,另外一套資源的路徑是"Library/unity default resources",同樣可以使用AssetDataBase.LoadAllAssetsAtPath()來加載,主要包含是GUI,內置Mesh這些資源以及一些未知的資源。兩者的guid也各不相同:

  1. Resources/unity_builtin_extra下資源對應的guid始終爲0000000000000000f000000000000000。
  2. Library/unity default resources下資源對應的guid始終爲0000000000000000e000000000000000。

個別資源冗餘的原因是中發現一些本應屬於Resources/unity_builtin_extra的Material和Shader,被引用時guid卻是0000000000000000e000000000000000,嘗試將其修改爲0000000000000000f000000000000000後發現並無差異。具體原因不明,猜測和Unity的版本、開發平臺(Windows,Mac)有一定的關係。爲了解決這個問題,我們對Material和Shader做了“兼容性”處理,在建立內置資源與提取出的內置資源映射關係時,手動添加一份對Library/unity default resources的映射。最終測試結果:

請輸入圖片描述

四、總結

優點:使用了引擎內部的一些機制,具有高度的可控性,在框架層面上實現了零冗餘打包。
缺點:依賴於引擎的內部實現,每次升級版本後都需要加以維護;相關資料匱乏,遇到問題不易解決。


這是侑虎科技第202篇原創文章,作者張迪,博客:http://blog.csdn.net/zhangdi2017。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ羣:793972859)

同時,作者也是U Sparkle活動參與者,UWA歡迎更多開發朋友加入U Sparkle開發者計劃,這個舞臺有你更精彩!

 

 

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