翻譯:莫銘
原文地址:AssetBundle usage patterns
本系列中的上一篇文章覆蓋了AssetBundle的基礎知識,尤其是各種加載API的底層行為。這篇文章討論的則是實際應用中使用AssetBundles可能遇到的,方方面面的問題與解決方法。
4.1. 管理已加載Assets
在內存緊張的環境中,小心控制加載Objects的大小和數量尤為重要。Objects被移出激活的場景時,Unity不會自動卸載他們。Asset的清理會在特定的時間觸發,當然也可以手動觸發。
必須小心的管理AssetBundles自身文件。一個AssetBundle在本地存儲(不論是在UnityCache中,還是通過AssetBundle.LoadFromFile加載的文件)中以一個文件的形式存在時,其占用的內存開銷很小,幾乎不會超過10-40kb。但如果有大量的AssetBundles存在,這開銷依舊不容忽視。
因為多數工程允許用戶重復體驗某內容(比如重玩一個關卡),所以知道什么時候去加載或卸載一個AssetBundle就尤為重要了。如果一個AssetBundle被不恰當的卸載了,這可能會引起Object在內存中存重復存在。不恰當的卸載AssetBundle在某些情況下也會導致與期望不符的表現,比如:引起紋理的缺失。想要知道為什么會發生這些,請參閱Assets,Objects和序列化文章中的段落Object之間的引用。
管理Assets和AssetBundles時,最重要的事情莫過于清楚,調用AssetBundle.Unload時傳入參數true或false,分別會發生什么情況,有何不同。
這個API在調用時會將對應AssetBundle的頭信息卸載掉。其參數標記是否也去卸載掉那些從該AssetBundle實例化的Objects。如果參數是true,那么所有從這個AssetBundle創建的Objects,即使正在激活的場景中使用,也會被立即卸載掉。
舉例來說,假設材質M從AssetBundleAB中加載,并且假設M當前正在激活的場景中。

如果AB.Unload(true)被調用,那么M將會從場景中被移除,銷毀和卸載。如果AB.Unload(false)被調用,那么AB的頭信息會被卸載,但是M依然留在場景中,并且將會一直有效。調用AssetBundle.Unload(false)會打斷M和AB之間的連接。如果AB稍后再次加載,那AB中的Objects會以新的身份被重新加載進內存。

如果AB稍后被再次加載,那么重新加載的是AssetBundle頭信息的新副本。M并不是從AB新副本中加載的。Unity不會去在AB新副本和M之間建立任何連接。

如果調用AB.LoadAsset()去重新加載M,Unity不會將舊的M副本解釋為AB中的實例數據。所以Unity會去加載一個新的M副本,因此這里會有兩個完全一樣的M副本存在在場景中。

對于大多數項目來說,這不是想要的行為。大多數項目應該使用AssetBundle.Unload(true),并且用一些方法確保這些Objects不會重復出現。常見的兩種方法:
- 在應用生命周期中,一些明顯的界限點(不同場景之間,或加載界面中)上,將那些短暫的(不是全局存在的基礎包)AssetBundles卸載掉的。這是最簡單和常見的選項。
- 單獨地為每個Objects維護引用計數,只有當AssetBundle中所有Objects都未被使用時,才去卸載掉它(AssetBundle)。這就允許應用去卸載和重載單獨的Objects,而不會出現重復的內存。
如果一個應用必須使用AssetBundle.Unload(false),那么只能通過下面兩種方法將單獨的Objects卸載:
- 消除這個不想要的Object的所有引用,場景中和代碼中的都要清除掉。都做好了,就可以調用Resources.UnloadUnusedAssets了。
- 以非疊加的方式加載一個場景。這樣會銷毀當前場景中的所有Objects,然后自動調用Resources.UnloadUnusedAssets。
如果一個項目有明顯的點,可以讓用戶等待Objects的加載和卸載,比如:游戲的不同模式之間,或關卡之間。這些點可以用來盡可能的卸載Objects,然后加載新的Objects。
要做到這點最簡單的方法就是,將工程分割成一塊塊的場景,然后將這些場景連同他們的依賴項打包到AssetBundles。應用進入到一個加載場景,完全卸載那個包含老場景的AssetBundle,然后加載包含新的場景的AssetBundle。
這種流程太簡單了,而一些項目需要更為復雜的AssetBundle管理。每個項目的數據是不同的,這里并沒有統一的AssetBundle設計模式。當決定如何分類Objects,將他們打包到AssetBundles時,一般來說開始時,最好以那些需要同時加載或更新的Objects打包在一起為原則。
舉例來說,想象一個角色扮演游戲。除了一些大多場景都會用到的Objects,單獨的地圖和過場動畫可以按場景歸類到AssetBundles。但是一些Objects會被多個場景需求。也可以將AssetBundles打包成肖像包,UI包和不同的角色模型和紋理。后者,這些共用的Objects和Assets可以打包到第二組AssetBundles,在啟動時加載,并在應用的整個運行期間保持加載狀態。
如果Unity在一個AssetBundle被卸載后,又需要從這個AssetBundle中重新加載一個Object,那么將會出現另一個問題。在這個情況下,加載將會失敗,這個Object將會以一個(Missing)Object的形式出現在Unity編輯器的層級面板中。
這種情況主要發生在:Unity失去再重獲圖形上下文控制權的時候,比如:移動app被暫停,或用戶鎖住PC的時候。這個時候,Unity必須重新上傳紋理和shaders到GPU中才行。如果此時,這些assets的源AssetBundle不可用了,那么應用將以品紅色(“missing shader”)渲染這些場景中Objects。
4.2. 發布
有兩種基本的方法將項目的AssetBundles發布到客戶端:隨項目一起安裝或在安裝后進行下載。是否要隨包安裝,這取決于空間大小和項目所在的平臺限制。移用應用一般選擇安裝后下載,來減少初始安裝包的大小,并低于無線下載大小限制。控制臺和PC項目一般都是將AssetBundle放在安裝包中。
適當的體系結構允許你在安裝后,將新的或修訂后的內容以補丁的形式放入項目中,而不用在乎AssetBundles一開始是如何遞交的。更多關于這方面的信息,可以查看本文中的段落用AssetBundle打補丁
4.2.1. 隨項目安裝
將AssetBundles依附在項目中,是發布他們最簡單的方法,因為這樣就不需要額外的下載管理代碼了。讓項目可以在安裝時包含AssetBundles,有兩個主要原因:
- 減少項目構建時間,允許簡單的迭代開發。如果這些AssetBundles不需要單獨更新,那么AssetBundles可以直接包含在應用中,通過Steaming Assets的形式存儲在應用中。詳情后面的Streaming Assets段落。
- 可更新內容的初始版本。一般這么做是為了減少用戶在初始安裝后的時間,或作為后續更新的基礎從而節約時間。這種情況下使用Streaming Assets并不理想。然而,自己寫個下載和緩存系統又不現實,那么可更新內容的初始版本可以從StreamingAsset加載進Unity緩存中。
4.2.1.1. Streaming Assets
想在安裝時內容就已包含在Unity應用中,最簡單的方法就是在構建項目之前,將他們放到/Assets/StreamingAssets/文件夾中。在StreamingAssets文件夾中的任何東西都會在構建時拷貝到最終應用中。這個文件夾可以用來存儲會出現在最終應用的內容,什么類型都可以,而不僅僅是AssetBundles。
StreamingAssets文件夾在本地存儲上的全路徑可以在運行時通過Application.streamingAssetsPath去訪問。這樣AssetBundles就可以在大多平臺上通過AssetBundle.LoadFromFile去加載啦。
Android開發者:在Android時,Application.streamingAssetPath將會指向一個壓縮的.jar文件,即使(譯者注:event if,但是感覺意思是“就像”)AssetBundles是被壓縮的。在這個情況下,必須用WWW.LoadFromCacheOrDownload去加載每個AssetBundle。當然也可以自己寫段代碼將.jar文件解壓,把其中的AssetBundle抽到本地內存上一個可讀的地方。
備注:StreamingAssets在一些平臺上不是一個可寫的位置。如果項目的AssetBundles在安裝后需要更新,要么使用WWW.LoadFromCacheOrDownload,要么自己寫個downloader。更多細節可查看訂制Downloader - 存儲篇。
4.2.2. 安裝后下載
移動設備上最受歡迎的AssetBundles交付方法還是在應用安裝后進行下載。這樣允許在用戶安裝后更新或添加新的內容,而不用強制用戶去重新下載整個應用。在移動平臺上,應用必須經過一個痛苦而耗時的認證過程(審核)。因此,開發一個好的系統來支持安裝后下載,至關重要。
交付AssetBundle最簡單的方法,就是把他們放在一個web服務器上,然后通過WWW.LoadFromCacheOrDownload或UnityWebRequest去傳遞。Unity會在本地存儲中自動緩存下載好的AssetBundles。如果下載的AssetBundle是LZMA壓縮格式,為了之后更快的加載,緩存中的AssetBundle是被解壓過的。如果下載下來的包是LZ4壓縮的,在緩存中AssetBundle將會保持壓縮格式不變。
如果緩存滿了,Unity將會從緩存里把最不常用的AssetBundle刪除。更多細節可以查看段落內置緩存。
請注意WWW.LoadFromCacheOrDownload是有瑕疵的。在加載AssetBundles中有提到,WWW對象在下載AssetBundle時,將消耗等同于AssetBundle數據大小的內存。這會導致不可接受的內存峰值。有三種方法可以避免這種情況:
- 讓AssetBundle小點。在AssetBundle下載時,其大小就決定了項目的內存預算。那些需要下載的應用,相比于直接從包中讀取AssetBundle的應用,需要分配更多的內存來下載AssetBundle。
- 如果正在使用Unity5.3或更新的版本,改用新的接口UnityWebRequest的DownloadHandlerAssetBundle,這樣就不會在下載時引起內存峰值了。
- 自己寫個Downloader。更多的細節請看章節定制下載器。
一般來說,建議剛開始時還是盡可能地使用UnityWebRequest,或者Unity5.2版本及之前的WWW.LoadFromCacheOrDownload。只有對那些很在意內置API的內存消耗,對緩存行為和表現都無法接受的;或者需要用平臺語言來達到要求的特殊項目來說,才需要在定制下載系統上下功夫。
說說幾個不能UnityWebRequest或WWW.LoadFromCacheOrDownload使用的情景:
- 需要對AssetBundle緩存進行細粒度控制的。
- 項目需要實現一個定制化的壓縮策略。
- 當項目需要使用平臺相關的API來滿足一些特殊需求,比如:在非激活狀態下流動數據。
-舉例:使用IOS后臺任務API,在后臺進行下載數據。 - 必須在一些Unity不支持SSL的平臺(比如PC)上通過SSL交付AssetBundles。
4.2.3. 內置緩存
Unity有內置的AssetBundle緩存系統用于緩存通過WWW.LoadFromCacheOrDownload或UnityWebRequest接口下載的AssetBundles。
這兩個接口都有個重載,接受一個AssetBundle版本號作為參數。這個數字沒有存在AssetBundle中,也不會由AssetBundle系統生成。
緩存系統記錄最近通過WWW.LoadFromCacheOrDownload或UnityWebRequest傳遞的版本號。不論哪個接口調用時跟隨一個版本號,緩存系統都會去檢查,看是否有已緩存好的AssetBundle。如有有的話,就會去對比版本號,如果版本號匹配,系統將直接加載緩存的AssetBundle。如果不匹配,或者沒有其他緩存好的AssetBundle了,Unity就會去下載一個新的副本[1]。然后將這個新的拷貝與這個新的版本號關聯起來。
AssetBundles在緩存系統中只以他們的文件名作為唯一標識,而不是以下載地址作為標識。這就意味著,一個同名的AssetBundle可以存儲在多個不同的地方。比如,一個AssetBundle可以放在內容交付網絡中的多個服務器上。只要文件名一樣,緩存系統就會認為他們是同一個AssetBundle。
每個應用應該自己決定一個合適的策略來給AssetBundle賦值一個版本號,并且將這個數字傳給WWW.LoadFromCacheOrDownload。大多數應用可以使用Unity5的AssetBundleManifestAPI。這個API通過AssetBundle的內容計算出一個MD5哈希碼,作為每個AssetBundle的版本號。只要一個AssetBundle發生了變化,他的哈希值也會變化,這就意味著這個AssetBundle可以被下載。
備注:由于Unity內置緩存的實現中有個怪癖,直到緩存滿了才會刪除老的AssetBundles。Unity打算在未來的版本中解決這個怪癖。
更多細節查看用AssetBundles打補丁。
Unity的內置緩存可以通過調用Caching對象中的API去控制。Unity緩存的行為可以通過改變Caching.expirationDelay和Caching.maximumAvailableDiskSpace去控制。
Caching.expirationDelay是在AssetBundle被自動刪除前必須等待的秒數。如果一個AssetBundle在這段時間中都沒有再被訪問過,他將會被自動刪除。
Caching.maximumAvailableDiskSpace用來決定Cache在本地存儲中有多少可用空間,在這些空間被填滿前,即使AssetBundle超過了Caching.expirationDelay的設定時間也不會被刪除。以字節為單位。當到達了這個限制,Unity就會將緩存中刪除那些最近不常用的AssetBundle(或者通過Caching.MarkAsUsed標記為使用)。Unity將會刪除緩存的AssetBundles直到有足夠的空間完成新的下載。
備注:直到Unity5.3,對于內置Unity緩存的控制都不能細到可以從緩存中移除指定的AssetBundles。他們只能通過:過期,超出硬盤空間或者調用Caching.CleanCache進行刪除。(Caching.CleanCache將會刪除當前緩存中的所有AssetBundles。)這在開發或線上操作時可能會引發問題,因為Unity不會自動刪除應用不再需要的AssetBundles。
4.2.3.1. 預備緩存
因為AssetBundles由他們的文件名進行標識,所以將隨包安裝的AssetBundle作為Cache的初期版本。可以通過將初始(基礎)版本的AssetBundles放在/Assets/StreamingAssets/中來完成。這個過程就和段落 隨項目安裝 中說的一樣。
在應用第一次運行時,可以通過從Application.streamingAssetsPath加載AssetBundles填充Cache。從這之后,應用就可以正常調用WWW.LoadFromCacheOrDownload或UnityWebRequest了。
4.2.3. 定制Dowloaders
自己寫個客制化的downloader可以讓應用完全控制AssetBundles是如何下載,解壓和存儲的。我們只建議那些正在寫大型項目的大型團隊去自己定制downloader。在寫一個定制downloader時,有四個需要思考的主要問題。
- 如何下載AssetBundles
- 在哪存儲AssetBundles
- 如果需要的話,如何去壓縮AssetBundles
- 如何為AssetBundles打補丁
關于補丁AssetBundles的信息,請查看段落用AssetBundles打補丁。
4.2.3.1. 下載
對于大多數應用來說,HTTP是用來下載AssetBundles最簡單的方法。然而實現一個以HTTP為基礎的downloader并不是一個簡單的任務。定制downloaders必須防止過度的內存分配,過量的線程使用以及喚醒。Unity的WWW類就是一個反例,就像這里描述的一樣。因為WWW會消耗太多的內存,如果應用不需要使用WWW.LoadFromCacheOrDownload,那就該禁止使用Unity的WWW類。
在寫定制化的downloader時,有三個選擇:
- C#的HttpWebRequest和WebClient類
- 定制原生插件
- Asset商店的包
4.2.3.1.1. C# 類
如果一個應用不需要支持HTTPS/SSL,C#的WebClient類提供了一個也許是最簡單的下載AssetBundles的機制。他可以直接將任何文件異步下載到本地存儲,而不會創建太多的托管內存。
要使用WebClient下載一個Asset Bundle,可以直接創建一個實例,并傳入AssetBundle的下載地址,還有目標路徑。如果需要控制更多的請求參數,就可以用C#的HttpWebRequest類去寫這個downloader:
- 從HttpWebResponse.GetResponseStream獲取字節流。
- 在棧上分配一個固定大小的緩存。
- 從響應中讀取數據流到緩存中。
- 使用C#的File.IO接口或者其他流讀寫系統,將緩存寫入硬盤。
平臺備注:只有在IOS,Android和WindowsPhone中,Unity C# runtime的HTTP類才支持HTTPS/SSL。在PC上,通過C#類訪問一個HTTPs服務器將會導致證書驗證錯誤。
4.2.3.1.2. Asset商店的包
一些asset商店中的包通過原生代碼,實現了可以通過HTTP,HTTPS和其他協議來下載文件。在你打算自己為Unity寫原生代碼插件時,建議你先評估下Asset商店中可用的包。
4.2.3.1.3. 定制原生插件
自己寫原生插件是在Unity中下載數據,最費勁,也最靈活的方法。由于需要很多的編程時間和技術風險,這個方法只有在其他方法都無法滿足應用需求的時候,我們才會推薦給你。比如:Windows,OSX和Linux平臺下,Unity不支持C#的SSL功能,而應用又必須使用SSL通訊時,才有必要自己去寫原生插件。
定制化的原生插件一般都會調用目標平臺的原生下載接口。比如IOS的NSURLConnection,和Android平臺的java.net.HttpURLConnection。想了解這些API更多的細節,就需要去查閱每個平臺的原生文檔。
4.2.3.2. 存儲
在所有平臺中,Application.persistentDataPath指向一個可寫的位置,可以用來持久化存儲數據。在寫一個定制化的downloader時,強烈建議在Application.persistentDataPath的子目錄中存儲下載的數據。
Application.streamingAssetPath不可寫,并且作為AssetBundle緩存它也是個糟糕的選擇。說幾個streamingAssetsPath位置的例子:
- OSX:在.app包中;不可寫
- Windows:在安裝的目錄中(比如:Program Files);一般不可寫
- IOS:在.ipa包里;不可寫
- Android:在壓縮的.jar文件中;不可寫
4.3. Asset分配策略
決定如何劃分項目的assets到AssetBundle,并不簡單。人們很容易采取一個過于簡單的策略,比如每個都打成一個AssetBundle或者都打到一起去,但這些方案都有明顯的缺點:
- AssetBundles太少...
- 增加運行時的內存占用
- 增加加載時間
- 下載量太大
- AssetBundle太多...
- 增加構建時間
- 使開發過于復雜
- 增加總下載時間
如何分類那些打包進AssetBundles中的Objects,是關鍵性的決定。主要策略有:
- 邏輯單元
- Object類型
- 并發內容
其實一個項目也可以針對不同的內容采用不同的策略。比如,一個項目可以將UI元素根據不同平臺分類,而交互內容按場景分類。不管采用什么策略,這有一些很好的指導:
- 將經常更新的對象與不經常更新的對象分開打包到不同的AssetBundles中。
- 將那些可能會同時加載的對象歸類到一起
舉例:一個模型,它的動畫還有它的紋理。
- 如果一個Object是多個Object的共同依賴項,而這些Object在幾個不同的AssetBundles中,那么將這個Object單獨打包在一個AssetBundle中。
- 理想情況下,將子Objects和他們的父Objects分為一類。
- 如果兩個Objects不會同時加載,比如一張紋理的高清和標清版本,將他們打包到不同的AssetBundles。
- 如果一些Objects是同個Object的不同版本(不同導入設置,或數據)。考慮使用AssetBundle Variants,而不是打包到不同的AssetBundle。
按照上面的指導做了之后。如果一個AssetBundle被加載了,那么不管什么時候,其中百分之五十以上的內容都應該已經被加載了,否則就要考慮繼續細分一下這個AssetBundle了。也要考慮合并那些比較小的(其中的assets小于5-10個),且已經被同時加載了的AssetBundles。
4.3.1 邏輯單元分組
邏輯單元分組是依據Objects在項目中的功能進行分類的。當采用這種策略時,應用的不同部分被分到不同的AssetBundles。
舉例:
- 將那些用于UI的材質和布局數據打包在一起
- 將一套角色的紋理,模型和動畫打包在一起
- 將那些很多場景會共用到的風景塊的紋理和模型打包到一起
邏輯單元分類是比較常用的AssetBundle策略,尤其適合:
- DLC(資料片)
- 那些會在應用中,經常出現在很多地方的實體。
舉例:
- 常規字體,或者基礎UI元素
- 那些根據不同平臺或性能設置而變化的實體。
按邏輯實體分類的好處就是允許你方便地更新單獨的實體,而不需要重新下載那些未發生變化的內容。這就是該策略為什么那么適合用于DLC的原因。這個策略也往往是最節約內存的,因為應用只需要加載當前正在使用實體的相關AssetBundles。
但是,這個策略實現起來也是最棘手的,因為開發者對于分配個AssetBundles的Objects,必須很清楚其中每個Object什么時候、為什么被項目使用。
4.3.2. 類型分組
類型分組是最簡單的策略。這個策略中,類似或相同的類型的Objects被放在同一個AssetBundle中。比如,將幾種不同的音軌放在一份AssetBundle中,或者幾種不同的語言文件放在一份AssetBundle中。
雖然這個策略很簡單,但往往在構建時間,加載時間和更新上是最有效的。它常常用于小文件和會同時進行更新的文件(其中的文件,要變一起變),比如本地化文件。
4.3.3. 并發內容分組
并發內容分組的策略是:其中的內容會被同時加載和使用。這個策略常見于那些內容局部性很強的項目,所謂局部性很強就是內容在應用中某些特定的時間和空間之外很少或幾乎不會出現。比如一個關卡類的游戲,每個關卡有獨特的美術風格,角色和聲效。
實現并發內容分組最常用的方法就是按照場景分組,每個場景AssetBundle包含場景大部分或全部的依賴項。
對于那些內容并不是強局部性的項目,且內容會經常出現在不同點上的項目,一般將并發內容分組和邏輯實體分組一同使用。他們是最大利用給定AssetBundle內容的基本策略。
這個情景的例子:一個開放世界的游戲,角色隨機、分散地出生在世界空間中。這種情況下,很難預測哪些角色會同時出現,所以應該使用不同的策略。
4.4. 用AssetBundle打補丁
給AssetBundle打補丁就是簡單的下載一個新的AssetBundle然后替換掉原來那個。如果用WWW.LoadFromCacheOrDownload或UnityWebRequest去管理應用的緩存AssetBundle,只要簡單的傳遞一個不同的版本參數給對應的API就可以了。(具體細節可以查看上面給的腳本參考鏈接。)
補丁系統中更為困難的問題是如何檢測哪些AssetBundles應該被替換。一個補丁系統需要兩個信息列表:
- 一個是當前已下載的AssetBundles還有他們的版本信息列表。
- 一個是服務器上AssetBundles還有他們的信息列表。
補丁器應該從服務器上下載AssetBundles列表,然后比較他們。丟失的AssetBundle或版本信息發生變化的,應該重新下載。
Unity5的AssetBundle系統在構建完成時會額外創建一個AssetBundle.這個額外的AssetBundle包含一個AssetBundleManifest Object。這個清單Object包含一個AssetBundles的列表,以及他們的哈希值,這可以用來傳遞一份當前可用AssetBundles和版本信息列表給客戶端。關于AssetBundle清單包的更多信息,可以查看Unity手冊。
也可以自己寫個系統來檢測AssetBundles是否變化。大多數自己去寫這個系統的開發者,會為他們的AssetBundle文件列表,選擇一個行業標準的數據格式,比如JSON;以及使用C#的標準類去計算校驗碼,比如MD5。
4.4.1. 差別化補丁
從Unity5開始,Unity以固定的順序構建AssetBundles中的數據。這就允許應用使用定制化的downloader來實現差別化補丁。想要使用確定布局來構建AssetBundles,只要在調用BuildAssetBundles 接口時,傳入BuildAssetBundleOptions.DeterministicAssetBundle標記就可以了。
Unity沒有為差別化補丁提供任何內置的機制。并且在使用內置緩存系統時,不論使用WWW.LoadFromCacheOrDownload還是UnityWebRequest都不會進行差別化補丁。如果想要實現差別化補丁,就需要自己去寫downloader了。
4.4.2. IOS按需加載資源
按需加載資源是蘋果在IOS和TVOS設備提供內容的一個接口。它在IOS9設備上有效。它目前不是App Store上應用的要求,但TVOS應用程序需要按需加載資源。
蘋果的按需加載資源系統的概述可以在這找到Apple開發者網站.
從Unity5.2.1開始,對于App Slicing和按需資源的支持都建立在另一個Apple系統上:Asset Catalogs。構建IOS應用時,在UnityEditor中可以注冊一個回調函數得到一個文件列表,包含哪些被自動放入Asset Catalogs中和被賦予On-Demand Resources標簽的文件。
一個新的API:UnityEngine.iOS.OnDemandResources,提供在運行時獲取和緩存On-Demand Resources文件。一旦資源通過ODR接收到,就可以通過AssetBundle.LoadFromFile接口加載進Unity。
更多細節和示例工程,請看Unity論壇中的這篇帖子。
4.5. 常見陷阱
這節說下使用AssetBundles時,經常會出現的幾個問題。
4.5.1. Asset重復
Unity5在將Object打包進一個AssetBundle時,會先找到它的所有依賴項。這是通過Asset數據庫做到的。這份依賴項信息用來決定哪些Objects被包含到AssetBundle中。
明確分配到AssetBundle的Objects只會被打包到這個指定的AssetBundle。 當一個Object的 AssetImporter的assetBundleName 屬性是非空字符串時,那么這個Object就是“明確分配”的。這可以通過在Unity編輯器里 在Object的面板中選擇一個AssetBundle,或者在編輯器腳本中做到。
任何沒有明確分配的Object,將會被打包到那些依賴他的Object所在的AssetBundle中。
如果兩個不同的Object被賦予到兩個不同的AssetBundle中,并且他們倆都引用一個共同的依賴項,那么這個被依賴的Object將會被拷貝到這兩個AssetBundles中。多出來重復的那個依賴項Object也會被實例化,這就意味著這個依賴項的兩個拷貝被認為是不同的對象,擁有不同的標識。這會增加應用AssetBundle包的整體大小。如果這兩個依賴項的父Objects被加載,那么這個對象的兩個不同的拷貝都會被加載進內存。
這有幾個方法可以解決這個問題:
- 確保打包進不同AssetBundles的Objects沒有共用的依賴項。那些擁有相同依賴項的對象可以打包在一起,不會重復打包依賴項。
- 對于那些擁有很多公用依賴項的項目來說,這個方法并不可行。這種方法生成的巨大的AssetBundle,必須經常重新打包、重新下載來保持方便和效率。
- 分割AssetBundles,來確保共用同一個依賴項的兩個AssetBundles不會同時被加載。
- 確保所有的依賴項資源被打包到他們自己的AssetBundles。這樣可以完全消除重復assets的風險,但是也引入了復雜性。應用必須追蹤AssetBundles之間的依賴關系,并且要確保在任何時候調用AssetBundle.LoadAsset,適當的AssetBundles都已經被加載了。
在Unity5,Object的依賴項可以通過UnityEditor命名空間中的AssetDatabaseAPI去追蹤。就像命名空間的名字一樣,這個API只能在Unity編輯器中使用,不能在運行時使用。AssetDatabase.GetDependencies可以用來查找一個Object或Assets的直接依賴項。注意這些依賴項可能也有他們自己的依賴項。此外,AssetImporterAPI可以用來查詢某個Object被分配到的AssetBundle。
組合使用AssetDatabase和AssetImporter接口,可以寫個編輯器腳本來:確保一個AssetBundle的所有直接或間接依賴項都已經分配到AssetBundles了;確保兩個AssetBundles共享的依賴項已經分配到一個AssetBundle了。由于重復assets會導致內存消耗,建議所有的項目都有一個這樣的腳本。
4.5.2 精靈集復制
下面的節段描述了Unity5的計算asset依賴項的代碼和自動生成的精靈圖集,一起使用時的奇怪現象。Unity5.2.2p4和Unity5.3已經修復了這種行為。
Unity5.2.2p4, 5.3或之后的版本
分配任何自動生成的精靈圖集到一個AssetBundle時,會包含精靈圖集中的精靈Objects。如果精靈Objects被分配到多個AssetBundles,那么精靈圖集將不會只分配到一個AssetBundle,會重復。如果精靈Objects沒有被分配到AssetBundle,那么精靈圖集也不會被分配到AssetBundle。
為了確保精靈圖集沒有重復出現,確保標記到同一個精靈圖集的所有精靈,被分配到同一個AssetBundle中。
Unity5.2.2p3和更早的版本
自動生成的精靈圖集不能分配給AssetBundle。因此,他們將會被包含到任何引用或包含其下精靈的AssetBundles中。
因為這個問題,強烈建議那些使用Unity精靈打包器的Unity5項目,升級到Unity5.2.2p4,5.3或更新的Unity版本。
對于那些無法升級的項目,有兩個變通的方法可以解決這個問題:
- 簡單:避免使用Unity的內置精靈打包器。用外部工具打包精靈圖集,然后做為普通Assets恰當的分配給一個AssetBundle。
- 困難:將所有使用圖集中精靈的Objects作為精靈分配給相同的AssetBundle。
- 這必須確保生成的精靈圖集不作為任何AssetBundle的間接依賴,這樣就不會重復了。
- 這個解決方案保留了使用Unity精靈打包器的簡單工作流程,但是它阻礙了開發者把Assets打包到不同AssetBundles,而且引用圖集的那些組件上,只要有數據發生變化,就必須重新下載整個精靈圖集,即使圖集沒有任何數據變化。
4.5.3. Android紋理
由于Android生態系統中的設備碎片很嚴重,通常都需要將紋理壓縮成幾種不同的格式。雖然所有的Android設備都支持ETC1,但是ETC1不支持紋理帶透明通道。如果一個應用不需要OpenGL ES2的支持,那解決這個問題最簡單的方法就是ETC2,它被所有Android OpenGL ES3設備所支持。
大多數應用需要在不支持ETC2的舊設備上運行。可以使用Unity5的AssetBundle Variants作為一個解決方法。(有關其他選項的詳細信息,請參閱Unity的Android優化指南。)
要使用AssetBundle Variants,就需要把所有不能使用ETC1壓縮的紋理,單獨分配到只有紋理的AssetBundles中。接下來,用供應商指定的紋理壓縮格式(如:DXT5,PVRTC和ATITC),來創建這些格式的AssetBundle Variants來支持不兼容ETC2格式的部分Android系統。每個AssetBundle Variants中的紋理,在TextureImporter中根據所在的Variants包設置對應的壓縮格式。
在運行時,可以通過SystemInfo.SupportsTextureFormat API來檢測所支持的不同紋理壓縮格式。這個信息可以用來選擇和加載AssetBundle Variants(包含系統支持的紋理壓縮格式)。
更多關于Android紋理壓縮格式的信息可以在這找到。
4.5.4. IOS文件句柄過度使用
本節中描述的問題在Unity5.3.2p2中已經修復。最新版本的Unity不會受到這個問題的影響。
在Unity5.3.2.p2版本之前,Unity在AssetBundle被加載后,將始終保留AssetBundle的打開文件句柄。這在大多數平臺上都不是一個問題。但是IOS限制了一個進程同時打開的文件句柄數不能超過255。如果加載AssetBundle時到超過了這個限制,將會加載失敗,得到一個“太多打開文件句柄”的錯誤。
對于那些試著將他們的內容細分為成百上千個AssetBundles的項目來說,這是個常見的問題。
對于那些不能將Unity升級到已經修復好的版本的項目來說,臨時解決方案如下:
- 通過合并相關AssetBundles來減少會用到的AssetBundles的數量
- 使用AssetBundle.Unload(false)來關閉AssetBundle的文件句柄,然后手動管理已加載Objects的生命周期。
4.6. AssetBundle Variants
Unity5的AssetBundle系統中一個關鍵特性就是AssetBundle Variants。應用可以通過Variants調整它的內容,來更好的適配當前的運行環境。Variants允許不同AssetBundle中不同的UnityEngine.Objects在加載和解決實例ID引用時,被認為是同一個Object。概念上,允許兩個UnityEngine.Objects出現時分享一樣的文件GUID和Local ID,然后通過一個VariantID字符串作為實際加載UnityEngine.Object的標識。
這個系統有兩個主要的用例:
-
Variants簡化了為指定平臺加載AssetBundles。
- 示例: 構建系統可以創建一個AssetBundle,其中包含的高分辨率紋理和適用于獨立DirectX11 Windows的復雜Shaders,而另一個AssetBundle包含專為Android準備的低保真內容。在運行時,項目資源加載代碼可以根據當前平臺加載對應的AssetBundle Variant,而傳入AssetBundle.Load接口的Object名稱不需要任何變化。
-
Variants可以使應用在同個平臺,針對不同硬件加載不同的內容。
- 這是支持大量移動設備的關鍵。在實際應用中,iPhone4和iPhone6不能顯示相同保真度的內容。
- 在Android平臺,AssetBundle Variants可以用來處理設備間大量不同的屏幕高寬比和DPIs。
4.6.1. 局限性
AssetBundle Variant系統的一個關鍵約束就是需要從不同的Asset來構建Variants。這個限制發生在,那些只是導入設置參數不同的Assets上。如果打包進Variant A和Variant B中的一個紋理,僅僅是因為要在Unity紋理導入時選擇不同的壓縮算法選項,那么就需要存在兩份不一樣的Assets,這就意味著Varient A和Varient B的這個紋理在硬盤上必須是不一樣的文件。
這個限制使得大型項目的管理復雜化,因為同個Asset的多份拷貝要同時被維護。當開發者想要改變Asset的內容時,就需要更新該Asset所有的拷貝。
這個問題沒有內置(官方)的解決方法。
大多數團隊實現他們自己的AssetBundle Variants版本。他們在構建AssetBundles時給文件名添加一個事先定好的后綴名,來識別AssetBundle的指定variant。一些開發者也已經擴展了他們定制的系統,以便能夠修改預制件上組件的參數。
4.7. 壓縮還是不壓縮?
是否要壓縮AssetBundles需要仔細考慮。重要的問題:
- AssetBundle的加載時間是一個關鍵因素嗎?從本地存儲或本地緩存中,加載非壓縮的AssetBundles要比壓縮的AssetBundles快很多。從遠端服務器下載壓縮的AssetBundles,一般要比下載非壓縮的AssetBundles快。
- AssetBundle的構建時間是一個關鍵因素嗎?LZMA和LZ4在壓縮文件時非常慢,而且Unity編輯器是一個個處理AssetBundles的。擁有大量AssetBundles的項目將會花費大量時間去壓縮他們。
- 應用的大小是個關鍵因素嗎?如果AssetBundle放在應用中,壓縮他們將會減少應用的整體大小。或者,在安裝后去下載AssetBundles。
- 內存使用是個關鍵因素嗎?Unity5.3之前,所有的Unity解壓機制都需要在解壓前將整個壓縮AssetBundle加載到內存。如果內存使用特別重要,那就用LZ4壓縮AssetBundles或者不壓縮。
- 下載時間是個關鍵因素嗎?只要在AssetBundles很大,或者假定用戶在帶寬受限的環境時(比如在移動設備上通過3G下載,或者在低速且計費的連接),壓縮才是有必要的。如果只有幾十兆的數據通過高速連接傳到PC上,那完全沒必要去進行壓縮。
4.8. AssetBundle和WebGL
Unity強烈建議開發者在WebGL項目中不要使用壓縮的AssetBundles
從Unity5.3起,WebGL項目中所有AssetBundle的解壓和加載必須發生在主線程。這是因為Unity5.3的WebGL導出選項目前不支持工作線程。(AssetBundles的下載交給瀏覽器通過JavascriptAPI XMLHttpRequest去下載,將不會發生在Unity的主線程中。)這意味著,在WebGL中加載壓縮后的AssetBundles開銷特大。
考慮到這點,你也許想避免使用默認的LZMA格式,而改用LZ4去壓縮你的AssetBundles,LZ4可以非常高效地按需解壓。如果你用LZ4交付同時又需要更小的壓縮文件,那么你可以配置你的Web服務器,在http協議中使用gzip壓縮這些文件(在LZ4壓縮之后再用gzip壓縮一遍)。[2]