原文地址:http://gad.qq.com/article/detail/7180936
作者:Loki+X
Unity資源機制
1、概述
本文意在闡述Unity資源機制相關的信息,以及一些關于個人的理解與試驗結果。另外還會提及一些因機制問題可能會出現的異常以及處理建議。大部分機制信息來源于官方文檔,另外為自我驗證后的結果。
2、資源
概述
Unity必須通過導入將所支持的資源序列化,生成AssetComponents后,才能被Unity使用。以下是Unity對Assets的描述:
Assets are the models,textures,sounds and all other “content”files from which you make your game。
資源(Asset)是硬盤中的文件,存儲在Unity工程的Assets文件夾內。有些資源的數據格式是Unity原聲支持的,有些資源則需要轉換為源生的數據格式后才能被使用。
對象(UnityEngine.Object),代表序列化數據的集合,表示某個資源的具體實例。它可以是Unity使用的任何類型的資源,所有對象都是UnityEngine.Object基類的子類
資源與對象時一對多的關系。
名稱 | 描述 | 支持格式 |
---|---|---|
Audio Clip | 音頻剪輯 | 音頻:.aif .wav .mp3 .ogg音軌:.xm .mod .it .s3m |
Cubemap Texture | 立方體貼圖紋理 | |
Flare | 耀斑 | |
Font | 字體 | .ttf |
Material | 材質 | |
Meshes | 網格 | .FBX .dae .3DS .dxf .obj |
Movie Texture | 電影貼圖 | .mov .mpg .mpeg .mp4 .avi .asf (導入需要QuickTime) |
Procedural Material Assets | 程序材質資源 | |
Render Texture | 渲染紋理 | |
Text Asset | 文本資源 | .txt .html .htm .xml .bytes |
Texture 2D | 二維紋理 | PSD TIFF JPG TGA PNG GIF BMP IFF PICT |
除此之外,想使用Unity不支持導入,或者未經導入的資源,只能使用IO Stream或者WWW 方法,這些將在下文對應欄目中說明。
注意:AssetBundle不是資源組件,故無法用資源組件的方式載入,只能使用WWW或者AssetBundle相關接口載入與讀取
GUID與fileID(本地ID)
Unity會為每個導入到Assets目錄中的資源創建一個meta文件,文件中記錄了GUID,GUID用來記錄資源之間的引用關系。還有fileID(本地ID),用于標識資源內部的資源。資源間的依賴關系通過GUID來確定;資源內部的依賴關系使用fileID來確定。
InstanceID(實例ID)
Unity為了在運行時,提升資源管理的效率,會在內部維護一個緩存表,負責將文件的GUID與fileID轉換成為整數數值,這個數值在本次會話中是唯一的,稱作實例ID(InstanceID)。
程序啟動時,實例ID緩存與所有工程內建的對象(例如在場景中被引用),以及Resource文件夾下的所有對象,都會被一起初始化。如果在運行時導入了新的資源,或從AssetBundle中載入了新的對象,緩存會被更新,并為這些對象添加相應條目。實例ID僅在失效時才會被從緩存中移除,當提供了指定文件GUID和fileID的AssetBundle被卸載時會產生移除操作。
卸載AssetBundle會使實例ID失效,實例ID與其文件GUID和fileID之間的映射會被刪除以便節省內存。重新載入AssetBundle后,載入的每個對象都會獲得新的實例ID。
** 資源的生命周期**
Object從內存中加載或卸載的時間點是定義好的。Object有兩種加載方式:自動加載與外部加載。當對象的實例ID與對象本身解引用,對象當前未被加載到內存中,而且可以定位到對象的源數據,此時對象會被自動加載。對象也可以外部加載,通過在腳本中創建對象或者調用資源加載API來載入對象(例如:AssetBundle.LoadAsset)
對象加載后,Unity會嘗試修復任何可能存在的引用關系,通過將每個引用文件的GUID與FileID轉化成實例ID的方式。一旦對象的實例ID被解引用且滿足以下兩個標準時,對象會被強制加載:
實例ID引用了一個沒有被加載的對象。
實例ID在緩存中存在對應的有效GUID和本地ID。
如果文件GUID和本地ID沒有實例ID,或一個已卸載對象的實例ID引用了非法的文件GUID和本地ID,則引用本身會被保留,但實例對象不會被加載。在Unity編輯器中表現為空引用,在運行的應用中,或場景視圖里,空對象會以多種方式表示,取決于丟失對象的類型:網格會變得不可見,紋理呈現為紫紅色等等。
** MonoScripts**
一個MonoScripts含有三個字符串:程序庫名稱,類名稱,命名空間。
構建工程時,Unity會收集Assets文件夾中獨立的腳本文件并編譯他們,組成一個Mono程序庫。Unity會將Assets目錄中的語言分開編譯,Assets/Plugins目錄中的腳本同理。Plugin子目錄之外的C#腳本會放在Assembly-CSharp.dll中。而Plugin及其子目錄中的腳本則放置在Assembly-CSharp-firstpass.all中。
這些程序庫會被MonoScripts所引用,并在程序第一次啟動時被加載。
3、資源文件夾
** Assets**
為Unity編輯器下的資源文件夾,Unity項目編輯時的所有資源都將置入此文件夾內。在編輯器下,可以使用以下方法獲得資源對象:
AssetDatabase.LoadAssetAtPath("Assets/x.txt");
注意:此方法只能在編輯器下使用,當項目打包后,在游戲內無法運作。參數為包含Assets內的文件全路徑,并且需要文件后綴。
Assets下的資源除特殊文件夾內,或者在會打入包內的場景中引用的資源,其余資源不會被打入包中。
Resources
資源載入
Assets下的特殊文件夾,此文件夾內的資源將會在項目打包時,全部打入包內,并能通過以下方法獲得對象:
Resources.Load("fileName");
Resources.Load("fileName");
注意:函數內的參數為相對于Resource目錄下的文件路徑與名稱,不包含后綴。Assets目錄下可以擁有任意路徑及數量的Resources文件夾,在運行時,Resources下的文件路徑將被合并。
例:Assets/Resources/test.txt與 Assets/TestFloder/Resources/test.png在使用Resource.Load("test")載入時,將被視為同一資源,只會返回第一個符合名稱的對象。如果使用Resource.Load(“test”)將返回text.txt;
如果在Resources下有相同路徑及名稱的資源,使用以上方法只能獲得第一個符合查找條件的對象,使用以下方法能或得到所有符合條件的對象:
Object[] assets = Resources.LoadAll("fileName");
TextAsset[] assets = Resources.LoadAll("fileName");
** 相關機制**
在工程進行打包后,Resource文件夾中的資源將進行加密與壓縮,打包后的程序內將不存在Resource文件夾,故無法通過路徑訪問以及更新資源。
依本文2.3章節所述,在程序啟動時會為Resource下的所有對象進行初始化,構建實例ID。隨著Resource內資源的數量增加,此過程耗時的增加是非線性的。故會出現程序啟動時間過長的問題,請密切留意Resource內的資源數量。
卸載資源
所有實例化后的GameObject 可以通過Destroy函數銷毀。請留意Object與GameObject之間的區別與聯系
Object可以通過Resources中的相關Api進行卸載
Resources.UnloadAsset(Object);//卸載對應Object
Resources.UnloadUnusedAssets();//卸載所有沒有被引用以及實例化的Object
注意以下情況:
Object obj = Resources.Load("MyPrefab");
GameObject instance = Instantiate(obj) as GameObjct;
......
Destroy(instance);
Resources.UnloadUnusedAssets();
此時UnloadUnusedAssets將不會生效,因為obj依然引用了MyPrefab,需要將obj = null,才可生效。
StreamingAssets
概述
StreamingAssets文件夾為流媒體文件夾,此文件夾內的資源將不會經過壓縮與加密,原封不動的打包進游戲包內。在游戲安裝時,StreamAssets文件件內的資源將根據平臺,移動到對應的文件夾內。StreamingAssets文件夾在Android與IOS平臺上為只讀文件夾.
你可以使用以下函數獲得不同平臺下的StreamingAssets文件夾路徑:
Application.streamingAssetsPath
請參考以下各平臺下StreamingAssets文件夾的等價路徑,Application.dataPath為程序安裝路徑。Android平臺下的路徑比較特殊,請留意此路徑的前綴,在一些資源讀取的方法中是不必要的(AssetBundle.LoadFromFile,下詳)
Application.dataPath+"/StreamingAssets"http://Windows OR MacOS
Application.dataPath+"/Raw" //IOS
"jar:file://"+Application.dataPath+"!/assets/" //Android
文件讀取
StreamingAssets文件夾下的文件在游戲中只能通過IO Stream或者WWW的方式讀取(AssetBundle除外)
IO Stream方式
using(FileStream stream = File.Open(Application.streamingAssetsPath + "fileName", FileMode.Open))
{
//處理方法
}
WWW方式(注意協議與不同平臺下路徑的區別)
using(WWW www = new WWW(Application.streamingAssetsPath + "fileName"))
{
yield return www;
www.text;
www.texture;
}
AssetBundle特有的同步讀取方式(注意安卓平臺下的路徑區別)
string assetbundlePath =
#if UNITY_ANDROID
Application.dataPath+"!/assets";
#else
Application.streamingAssetsPath;
#endif
AssetBundle.LoadFromFile(assetbundlePath+"/name.unity3d");
PersistentDataPath
Application.persistentDataPath
Unity指定的一個可讀寫的外部文件夾,該路徑因平臺及系統配置不同而不同。可以用來保存數據及文件。該目錄下的資源不會在打包時被打入包中,也不會自動被Unity導入及轉換。該文件夾只能通過IO Stream以及WWW的方式進行資源加載。
4、WWW載入資源
** 概述**
WWW是一個Unity封裝的網絡下載模塊,支持Http以及file兩種URL協議,并會嘗試將資源轉換成Unity能使用的AssetsComponents(如果資源是Unity不支持的格式,則只能取出byte[])。具體對應的格式參考第一章表格。WWW加載是異步方法。
byte[] bytes = WWW.bytes;
string text = WWW.text;
Texture2D texture = WWW.texture;
MovieTexture movie = WWW.movie;
AssetBundle assetbundle = WWW.assetBundle;
AudioClip audioClip = WWW.audioClip;
相關機制
** new WWW**
每次new WWW時,Unity都會啟用一個線程去進行下載。通過此方式讀取或者下載資源,會在內存中生成WebStream,WebStream為下載文件轉換后的內容,占用內存較大。使用WWW.Dispose將終止仍在加載過程中的進程,并釋放掉內存中的WebStream。
如果WWW不及時釋放,將占用大量的內存,推薦搭配using方式使用,以下兩種方式等價。
WWW www = new WWW(Application.streamingAssetsPath + "fileName");
try
{
yield return www;
www.text;
www.texture;
}
finally
{
www.Dispose();
}
using(WWW www = new WWW(Application.streamingAssetsPath + "fileName"))
{
yield return www;
www.text;
www.texture;
}
如果載入的為Assetbundle且進行過壓縮,則還會在內存中占用一份AssetBundle解壓用的緩沖區Deompresion Buffer,AssetBundle壓縮格式的不同會影響此區域的大小。
WWW.LoadFromCacheOrDownload
int version = 1;
WWW.LoadFromCacheOrDownload(PathURL+"/fileName",version);
使用此方式加載,將先從硬盤上的存儲區域查找是否有對應的資源,再驗證本地Version與傳入值之間的關系,如果傳入的Version>本地,則從傳入的URL地址下載資源,并緩存到硬盤,替換掉現有資源,如果傳入Version<=本地,則直接從本地讀取資源;如果本地沒有存儲資源,則下載資源。此方法的存儲路徑無法設定以及訪問。使用此方法載入資源,不會在內存中生成 WebStream(其實已經將WebStream保存在本地),如果硬盤空間不夠進行存儲,將自動使用new WWW方法加載,并在內存中生成WebStream。在本地存儲中,使用fileName作為標識符,所以更換URL地址而不更改文件名,將不會造成緩存資源的變更。
保存的路徑無法更改,也沒有接口去獲取此路徑
5、 AssetBundle
概述
AssetBundles let you stream additional assets via the WWW class and instantiate them at runtime. AssetBundles are created via BuildPipeline.BuildAssetBundle.
AssetBundle是Unity支持的一種文件儲存格式,也是Unity官方推薦的資源存儲與更新方式,它可以對資源(Asset)進行壓縮,分組打包,動態加載,以及實現熱更新,但是AssetBundle無法對Unity腳本進行熱更新,因為其需要在打包時進行編譯。
Assetbundle打包
平臺兼容性
AssetBundle適用于多種平臺,但不同平臺所使用的AssetBundle并不相同,在創建AssetBundle時需要通過參數來指定目標平臺,其關系如下表
Standalone | WebPlayer | IOS | Android | |
---|---|---|---|---|
Standalone | √ | √ | √ | √ |
WebPlayer | √ | √ | ||
IOS | √ | |||
Android | √ |
創建API
public enum BuildAssetBundleOptions
{
None = 0,
//Build assetBundle without any special option.
UncompressedAssetBundle = 1,
//Don't compress the data when creating the asset bundle.
CollectDependencies = 2,
//Includes all dependencies.
CompleteAssets = 4,
//Forces inclusion of the entire asset.
DisableWriteTypeTree = 8,
//Do not include type information within the AssetBundle.
DeterministicAssetBundle = 16,
//Builds an asset bundle using a hash for the id
ForceRebuildAssetBundle = 32,
//Force rebuild the assetBundles.
IgnoreTypeTreeChanges = 64,
//Ignore the type tree changes when doing the incremental build check.
AppendHashToAssetBundleName = 128,
//Append the hash to the assetBundle name.
ChunkBasedCompression = 256
//Use chunk-based LZ4 compression when creating the AssetBundle.
}
AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles("OutputPath", BuildAssetBundleOptions, tragetPlatform);
在Unity的5.3版本中,簡化了AssetBundle的打包方式,只留下了一個api與寥寥幾個設置參數,而之前最讓人頭痛的資源依賴管理,也被默認進行處理。 而在每個Asset文件的Inspector面板上都會多出一個Asset Labels的設定欄:
AssetBundle name:需要將此資源打包的AssetBundle名稱
AssetBundle Variant:需要將此資源打包的AssetBundle的變體名
Variant
Variant是5.3以后新添加的一個概念,這個值其實是一個尾綴,將添加在對應AssetBundle的名稱之后,如:ddzgame.hd,hd就是Variant(從此以后AssetBundle的尾綴已經跟其文件類型本身沒有任何聯系)。
自動打包腳本
從以上可知,如果需要一個一個的對資源設置AssetBundle Name與Variant實在太過繁瑣與麻煩,也可能出現紕漏,好在可以通過腳本去批量設置這兩個參數:
AssetImporter assetImporter = AssetImporter.GetAtPath("path");
assetImporter.assetBundleName = "Assetbundle Name";
assetImporter.assetBundleVariant = "Assetbundle Variant";
其中path是資源在Assets目錄下的路徑。
Scene打包
Scene打包跟資源打包無異,唯一需要注意的是:Scene只能與Scene打入同一個AssetBundle內,而無法與其他資源打入同一個AssetBundle。
PS:AssetBundle內的Scene需要在AssetBundle加載后,通過SceneManager來加載。
AssetBundle依賴
依賴機制
假設有AssetBundleA與 AssetBundleB兩個AssetBundle,AssetBundle中的資源引用了AssetBundleB中的資源,則稱AssetBundleA依賴于AssetBundleB。具體實例請看下圖注意被依賴AssetBundle需要加載的時機
注意其依賴的機制: AssetBundle中保存有其中所有資源的GUID,FileID等序列化信息,AssetBundle只會在內存中尋找其依賴資源所在的AssetBundle,并自動從中加載出所需資源。具體可參考本文2.3章節
** Manifest**
在前面有提到,在5.3中,Unity會自動處理AssetBundle中資源的依賴關系。在默認情況下,如果AssetBundle間有交叉的資源引用,不會再重復打包,在打包AssetBundle后,會發現其在輸出目錄多出了一個與目錄名稱相同的無后綴AssetBundle文件,其為自動生成的AssetBundleManifest文件,其內保存有此次生成的所有AssetBundle之間的依賴關系與清單。我們可以在載入這個AssetBundle后使用以下方法獲得此對象。
AssetBundle.LoadAsset("AssetBundleManifest");
Manifest保存有重要的依賴信息,在載入AssetBundle時,可以通過Manifest查詢其是否有依賴的AssetBundle,然后我們手動對其進行管理,避免依賴項丟失而出現bug
string[] fullnames = AssetBundle.GetDirectDependencies(fullname);
string[] fullnames = AssetBundle.GetAllDependencies(fullname);
Direct方法會返回所有直接依賴的AssetBundle名稱數組,All方法會返回所有依賴的AssetBundle名稱數組,fullname包括名稱與Variant。推薦使用Direct方法做遞歸處理,避免重復載入。
AssetBundle加載
加載方式
之前已經提及,不再詳細說明,使用WWW 或者 AssetBundle相關API加載,其中AssetBundle的API只能進行本地加載。
AssetBundle.LoadfromMemory(byte[] bytes)
此API是一個例外,用來對加密的Assetbundle進行讀取,可以結合WWW使用。
壓縮
LZMA(Ziv-Markov chain algorithm)格式
Unity打包成AssetBundle時的默認格式,會將序列化數據壓縮成LZMA流,使用時需要整體解包。優點是打包后體積小,缺點是解包時間長,且占用內存。
LZ4格式
5.3新版本添加的壓縮格式,壓縮率不及LZMA,但是不需要整體解壓。LZ4是基于chunk的算法,加載對象時只有響應的chunk會被解壓。
壓縮格式在打包時通過AssetBundleOption參數選擇。
內存占用
AssetBundle加載后會在內存中生成AssetBundle的序列化架構的占用,一般來說遠遠小于資源本身,除非包含復雜的序列化信息(復雜多層級關系或復雜靜態數據的prefab等)
AssetBundle卸載
卸載API
AssetBundle.Unload(bool unloadAllLoadedObjects);
AssetBundle只有唯一的一個卸載函數,傳入的參數用來選擇是否將已經從此AssetBundle中加載的資源一起卸載。另外,已經從AssetBundle中加載的資源可以通過Resources.UnloadAsset(Object)卸載。如果想通過Resources.UnloadUnusedAssets()卸載從AssetBundle加載的資源,一定要先將AssetBundle卸載后才能生效。
資源卸載總覽
當AssetBundle被卸載后,實例ID與其文件GUID和本地ID之間的映射會被刪除, 即其無法被其后加載的依賴于它的資源所查找及引用。詳情請參考本文2.3章節
** 案例分析**
案例1 游戲切換到后臺一段時候切回,出現shader或者Texture丟失。
在移動平臺,當程序切到主界面或者在后臺長時間運行時,GPU會自動對后臺程序的資源進行清理。如果shader或者Texture是從AssetBundle中加載出來,而此AssetBundle已經被卸載的話,Unity無法在程序恢復時從內存中加載這些資源,從而造成丟失。有人會問,這些資源不是已經加載到內存中了么?但是,他們在被加載到GPU之后會被從內存中清除。因此要防止此狀況最穩健的方法,就是在場景切換前,不要卸載掉其所屬的AssetBundle。
案例2 當經常使用AssetBundleB.Unload(false)卸載時,有時會發現AssetBundle中的資源在內存中有多份同時存在。
問題的根源在于從AssetBundle中加載出來的資源,在該AssetBundle卸載之后與其的聯系就斷開了。
例如:從AssetBundleA中加載出來一個Prefab p1,p1依賴資源tex1也會自動加載到內存中。然后用AssetBundle.Unload(false)卸載AssetBundleA,此時p1與AssetBundleA的聯系斷開。之后,從AssetBundleA中加載Prefab p2,p2也依賴資源tex1,那么在加載p2時tex1會再次被加載到內存中,導致重復。