你應該知道的AssetBundle管理機制
接上期AssetBundle打包的講解,我們今天為大家繼續探秘AssetBundle,從管理機制的角度出發,談談其資源載入和卸載的原理。
AssetBundle載入基礎
通過AssetBundle載入資源,分為兩步,第一步是獲取AssetBundle對象,第二步是通過該對象載入需要的資源。而第一步又分為兩種方式,下文中將結合常用的API進行詳細地描述。
一、獲取AssetBundle對象的常用API
(1)先獲取WWW對象,再通過http://WWW.assetBundle獲取AssetBundle對象:
- public WWW(string url);載入Bundle文件並獲取WWW對象,完成後會在內存中創建較大的WebStream(解壓後的內容,通常為原Bundle文件的4~5倍大小,紋理資源比例可能更大),因此後續的AssetBundle.Load可以直接在內存中進行。
- public static WWW LoadFromCacheOrDownload(string url, int version, uint crc = 0);載入Bundle文件並獲取WWW對象,同時將解壓形式的Bundle內容存入磁碟中作為緩存(如果該Bundle已在緩存中,則省去這一步),完成後只會在內存中創建較小的SerializedFile,而後續的AssetBundle.Load需要通過IO從磁碟中的緩存獲取。
- public AssetBundle assetBundle;通過之前兩個介面獲取WWW對象後,即可通過http://WWW.assetBundle獲取AssetBundle對象。
(2) 直接獲取AssetBundle:
- public static AssetBundle CreateFromFile(string path);通過未壓縮的Bundle文件,同步創建AssetBundle對象,這是最快的創建方式。創建完成後只會在內存中創建較小的SerializedFile,而後續的AssetBundle.Load需要通過IO從磁碟中獲取。
- public static AssetBundleCreateRequest CreateFromMemory(byte[] binary);通過Bundle的二進位數據,非同步創建AssetBundle對象。完成後會在內存中創建較大的WebStream。調用時,Bundle的解壓是非同步進行的,因此對於未壓縮的Bundle文件,該介面與CreateFromMemoryImmediate等價。
- public static AssetBundle CreateFromMemoryImmediate(byte[] binary);該介面是CreateFromMemory的同步版本。
- 註:5.3下分別改名為LoadFromFile,LoadFromMemory,LoadFromMemoryAsync並增加了LoadFromFileAsync,且機制也有一定的變化,可詳見Unity官方文檔。
二、從AssetBundle載入資源的常用API
- public Object Load(string name, Type type);通過給定的名字和資源類型,載入資源。載入時會自動載入其依賴的資源,即Load一個Prefab時,會自動Load其引用的Texture資源。
- public Object[] LoadAll(Type type);
一次性載入Bundle中給定資源類型的所有資源。
- public AssetBundleRequest LoadAsync(string name, Type type);該介面是Load的非同步版本。
- 註:5.x下分別改名為LoadAsset,LoadAllAssets,LoadAssetAsync,並增加了LoadAllAssetsAsync。
AssetBundle載入進階
一、介面對比:new WWW與http://WWW.LoadFromCacheOrDownload
(1)前者的優勢
- 後續的Load操作在內存中進行,相比後者的IO操作開銷更小;
- 不形成緩存文件,而後者則需要額外的磁碟空間存放緩存;
- 能通過http://WWW.texture,http://WWW.bytes,http://WWW.audioClip等介面直接載入外部資源,而後者只能用於載入AssetBundle
(2)前者的劣勢
- 每次載入都涉及到解壓操作,而後者在第二次載入時就省去了解壓的開銷;
- 在內存中會有較大的WebStream,而後者在內存中只有通常較小的SerializedFile。(此項為一般情況,但並不絕對,對於序列化信息較多的Prefab,很可能出現SerializedFile比WebStream更大的情況)
二、內存分析
在管理AssetBundle時,了解其載入過程中對內存的影響意義重大。在上圖中,我們在中間列出了AssetBundle載入資源後,內存中各類物件的分布圖,在左側則列出了每一類內存的產生所涉及到的載入API:
- WWW對象:在第一步的方式1中產生,內存開銷小;
- WebStream:在使用new WWW或CreateFromMemory時產生,內存開銷通常較大;
- SerializedFile:在第一步中兩種方式都會產生,內存開銷通常較小;
- AssetBundle對象:在第一步中兩種方式都會產生,內存開銷小;
- 資源(包括Prefab):在第二步中通過Load產生,根據資源類型,內存開銷各有大小;
- 場景物件(GameObject):在第二步中通過Instantiate產生,內存開銷通常較小。在後續的章節中,我們還將針對該圖中各類內存物件分析其卸載的方式,從而避免內存殘留甚至泄露。
三、注意點
- CreateFromFile只能適用於未壓縮的AssetBundle,而Android系統下StreamingAssets是在壓縮目錄(.jar)中,因此需要先將未壓縮的AssetBundle放到SD卡中才能對其使用CreateFromFile。
Application.streamingAsstsPath = "jar:file://" + Application.dataPath+"!/assets/";
- iOS系統有256個開啟文件的上限,因此,內存中通過CreateFromFile或http://WWW.LoadFromCacheOrDownload載入的AssetBundle對象也會低於該值,在較新的版本中,如果LoadFromCacheOrDownload超過上限,則會自動改為new WWW的形式載入,而較早的版本中則會載入失敗。
- CreateFromFile和http://WWW.LoadFromCacheOrDownload的調用會增加RersistentManager.Remapper的大小,而PersistentManager負責維護資源的持久化存儲,Remapper保存的是載入到內存的資源HeapID與源數據FileID的映射關係,它是一個Memory Pool,其行為類似Mono堆內存,只增不減,因此需要對這兩個介面的使用做合理的規劃。
- 對於存在依賴關係的Bundle包,在載入時主要注意順序。舉例來說,假設CanvasA在BundleA中,所依賴的AtlasB在BundleB中,為了確保資源正確引用,那麼最晚創建BundleB的AssetBundle對象的時間點是在實例化CanvasA之前。即,創建BundleA的AssetBundle對象時、Load(「CanvasA」)時,BundleB的AssetBundle對象都可以不在內存中。
- 根據經驗,建議AssetBundle文件的大小不超過1MB,因為在普遍情況下Bundle的載入時間與其大小並非呈線性關係,過大的Bundle可能引起較大的載入開銷。
- 由於WWW對象的載入是非同步的,因此逐個載入容易出現下圖中CPU空閑的情況(選中幀處Vsync佔了大部分),此時建議適當地同時載入多個對象,以增加CPU的使用率,同時加快載入的完成。
AssetBundle卸載
前文提到了通過AssetBundle載入資源時的內存分配情況,下面,我們結合常用的API來介紹如何將已分配的內存進行卸載,最終達到清空所有相關內存的目的。
一、內存分析
在上圖中的右側,我們列出了各種內存物件的卸載方式:
- 場景物件(GameObject):這類物件可通過Destroy函數進行卸載;
- 資源(包括Prefab):除了Prefab以外,資源文件可以通過三種方式來卸載:1) 通過Resources.UnloadAsset卸載指定的資源,CPU開銷小;2)通過Resources.UnloadUnusedAssets一次性卸載所有未被引用的資源,CPU開銷大;3)通過AssetBundle.Unload(true)在卸載AssetBundle對象時,將載入出來的資源一起卸載。而對於Prefab,目前僅能通過DestroyImmediate來卸載,且卸載後,必須重新載入AssetBundle才能重新載入該Prefab。由於內存開銷較小,通常不建議進行針對性地卸載。
- WWW對象:調用對象的Dispose函數或將其置為null即可;
- WebStream:在卸載WWW對象以及對應的AssetBundle對象後,這部分內存即會被引擎自動卸載;
- SerializedFile:卸載AssetBundle後,這部分內存會被引擎自動卸載;
- AssetBundle對象:AssetBundle的卸載有兩種方式:
1)通過AssetBundle.Unload(false),卸載AssetBundle對象時保留內存中已載入的資源;
2)通過AssetBundle.Unload(true),卸載AssetBundle對象時卸載內存中已載入的資源,由於該方法容易引起資源引用丟失,因此並不建議經常使用;
二、注意點
在通過AssetBundle.Unload(false)卸載AssetBundle對象後,如果重新創建該對象並載入之前載入過的資源到內存時,會出現冗餘,即兩份相同的資源。
被腳本的靜態變數引用的資源,在調用Resources.UnloadUnusedAssets時,並不會被卸載,在Profiler中能夠看到其引用情況。UWA推薦方案
通過以上的講解,相信您對AssetBundle的載入和卸載已經有了明確的了解。下面,我們將簡單地做一下API選擇上的推薦:
- 對於需要常駐內存的Bundle文件來說,優先考慮減小內存佔用,因此對於存放非Prefab資源(特別是紋理)的Bundle文件,可以考慮使用http://WWW.LoadFromCacheOrDownload或AssetBundle.CreateFromFile載入,從而避免WebStream常駐內存;而對於存放較多Prefab資源的Bundle,則考慮使用new WWW載入,因為這類Bundle用http://WWW.LoadFromCacheOrDownload載入時產生的SerializedFile可能會比new WWW產生的WebStream更大。
- 對於載入完後即卸載的Bundle文件,則分兩種情況:優先考慮速度(載入場景時)和優先考慮流暢度(遊戲進行時)。1)載入場景的情況下,需要注意的是避免WWW對象的逐個載入導致的CPU空閑,可以考慮使用載入速度較快的http://WWW.LoadFromCacheOrDownload或AssetBundle.CreateFromFile,但需要避免後續大量地進行Load資源的操作,引起IO開銷(可以嘗試直接LoadAll)。2) 遊戲進行的情況下,則需要避免使用同步操作引起卡頓,因此可以考慮使用new WWW配合AssetBundle.LoadAsync來進行平滑的資源載入,但需要注意的是,對於Shader、較大的Texture等資源,其初始化操作通常很耗時,容易引起卡頓,因此建議將這類資源在載入場景時進行預載入。
- 只在Bundle需要加密的情況下,考慮使用CreateFromMemory,因為該介面載入速度較慢。
- 盡量避免在遊戲進行中調用Resources.UnloadUnusedAssets(),因為該介面開銷較大,容易引起卡頓,可嘗試使用Resources.Unload(obj)來逐個進行卸載,以保證遊戲的流暢度。
需要說明的是,以上內存管理較適合於Unity 5.3之前的版本。Unity引擎在5.3中對AssetBundle的內存佔用進行一定的調整,目前我們也在進一步的學習和研究中。
以上即為我們這次為您帶來的AssetBundle管理機制,希望對您的項目研發有所幫助。我們會在後續技術文章通過大量的案例來進一步解釋AssetBundle的管理機制,敬請關注。
推薦閱讀:
※千億特徵流式學習在大規模推薦排序場景的應用
※Unity3D回合制手游《星辰奇緣》性能測評深度分析
※Unity匿名函數的堆內存優化
※一個採用GC的原生程序語言有沒有可能性能上超越非GC的原生程序語言?
※寫工業級別代碼是怎樣一種體驗?