你應該知道的AssetBundle管理機制

接上期AssetBundle打包的講解,我們今天為大家繼續探秘AssetBundle,從管理機制的角度出發,談談其資源載入和卸載的原理。

AssetBundle載入基礎

通過AssetBundle載入資源,分為兩步,第一步是獲取AssetBundle對象,第二步是通過該對象載入需要的資源。而第一步又分為兩種方式,下文中將結合常用的API進行詳細地描述。

一、獲取AssetBundle對象的常用API

(1)先獲取WWW對象,再通過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對象後,即可通過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與WWW.LoadFromCacheOrDownload

(1)前者的優勢

  • 後續的Load操作在內存中進行,相比後者的IO操作開銷更小;
  • 不形成緩存文件,而後者則需要額外的磁碟空間存放緩存;
  • 能通過WWW.textureWWW.bytesWWW.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或WWW.LoadFromCacheOrDownload載入的AssetBundle對象也會低於該值,在較新的版本中,如果LoadFromCacheOrDownload超過上限,則會自動改為new WWW的形式載入,而較早的版本中則會載入失敗。
  • CreateFromFile和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文件,可以考慮使用WWW.LoadFromCacheOrDownload或AssetBundle.CreateFromFile載入,從而避免WebStream常駐內存;而對於存放較多Prefab資源的Bundle,則考慮使用new WWW載入,因為這類Bundle用WWW.LoadFromCacheOrDownload載入時產生的SerializedFile可能會比new WWW產生的WebStream更大。
  • 對於載入完後即卸載的Bundle文件,則分兩種情況:優先考慮速度(載入場景時)和優先考慮流暢度(遊戲進行時)。

    1)載入場景的情況下,需要注意的是避免WWW對象的逐個載入導致的CPU空閑,可以考慮使用載入速度較快的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的原生程序語言?
寫工業級別代碼是怎樣一種體驗?

TAG:Unity游戏引擎 | 性能优化 | 手机游戏开发 |