本篇文章主要內容來自於官方教程 Assets, Resources and AssetBundles,介紹了 AssetBundle 的各種機制,使用方式和適用場景等html
有關其餘 Unity 資源管理的內容可見Unity學習—資源管理概覽git
文中全部 API 均以版本 2019.3 爲準github
本文原地址:Unity學習—AssetBundle算法
AssetBundle 是外部資產的集合,可獨立於 Unity 構建過程外,是 Unity 更新非代碼內容的主要工具,常常置於服務器上供用戶終端動態獲取;AssetBundle 使開發者能夠提交更小的應用包,最小化運行時內存壓力,使終端能夠選擇性加載優化內容c#
AssetBundle 主要由兩部分組成:文件頭和數據段數組
文件頭包含了id、壓縮類型、索引清單,該索引清單是與 Resources 相同的記錄了序列化文件中的字節偏移量的查找表。對於大部分平臺該表爲平衡搜索樹,對 Windows 和 OSX 系列(包括 iOS)則爲紅黑樹,隨着 AssetBundle 中對象的增長,構造清單所需時間的增加速度將超過線形增加速度緩存
數據段包含了 Asset 通過序列化的原始數據,數據還可選擇是否壓縮,若使用 LZMA 壓縮,則將全部 Asset 的字節數組總體壓縮;若使用 LZ4 壓縮,則將每一個 Asset 單獨壓縮;若不壓縮,則數據保持原始字節流服務器
有四種不一樣的 API 用於加載 AssetBundle,但每一個 API 的行爲隨壓縮算法和平臺而不一樣網絡
AssetBundle.LoadFromMemoryAsync併發
IEnumerator LoadFromMemoryAsync(string path)
{
AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
yield return createRequest;
AssetBundle bundle = createRequest.assetBundle;
var prefab = bundle.LoadAsset<GameObject>("MyObject");
Instantiate(prefab);
}
複製代碼
AssetBundle.LoadFromFile
該方法可高效地從硬盤加載未壓縮或 LZ4 壓縮的 Assetbundle,加載 LZMA 壓縮包時會先解壓再加載到內存
public class LoadFromFileExample : MonoBehaviour {
function Start() {
var myLoadedAssetBundle
= AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
if (myLoadedAssetBundle == null) {
Debug.Log("Failed to load AssetBundle!");
return;
}
var prefab = myLoadedAssetBundle.LoadAsset.<GameObject>("MyObject");
Instantiate(prefab);
}
}
複製代碼
WWW.LoadfromCacheOrDownload(5.6 及之前版本)
舊方法,已拋棄
UnityWebRequestAssetBundle (5.3 及之後版本)
先使用UnityWebRequest.GetAssetBundle
建立請求,再將請求傳入DownloadHandlerAssetBundle.GetContent(UnityWebRequest)
,下載完成後可像AssetBundle.LoadFromFile
同樣,直接使用 assetBundle 對象
該方法使開發者更靈活處理下載數據,選擇臨時存儲或長期緩存,避免沒必要要的內存使用。同時,因爲是原生代碼,沒有託管堆棧擴展風險,DownloadHandler 也不會保留下載數據,進一步減小了內存開銷
LZMA 壓縮包會在下載時解壓,並以 LZ4 從新壓縮緩存,可調用 Caching.CompressionEnabled 修改
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request
= UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
Instantiate(cube);
Instantiate(sprite);
}
複製代碼
官方推薦儘可能使用 AssetBundle.LoadFromFile
,該 API 在速度、磁盤使用和運行時內存使用方面都最高效;須要下載則使用 UnityWebRequest
同步異步加載 Asset 一共有六種 API 可以使用,同步方法必定比對應的異步方法快至少一幀
LoadAllAssets
適合加載包中大部分或全部獨立 Unity 對象時使用,相較於屢次重複調用另外兩種 API,LoadAllAssets
速度要稍快一點。所以當 Asset 數量巨大且一次性須要加載的 Asset 少於 2/3 的時候,建議將 AssetBundle 拆分紅多個小包體,再使用LoadAllAssets
加載
LoadAssetWithSubAssets
適合須要加載的對象內嵌了其餘對象的狀況,若加載對象均來自於一個 Asset 且包中有許多其餘無關對象,則使用該 API
其餘狀況均用LoadAsset (LoadAssetAsync)
Unity 對象加載時在主線程執行,對象數據是在工做線程 worker thread,任何線程不敏感的操做都在工做線程執行
異步加載時會根據時間片限制每幀加載多個對象,自 Unity 5.3 後,對象加載就並行化了。多個對象在工做線程被反序列化、處理和集成,當對象加載完成,則觸發 Awake 回調
同步加載方法 AssetBundle.Load
會暫停主線程知道加載完成,它們還將加載過程進行時間切片,以使對象集成所佔用的幀時間不超過特定的毫秒數,該值可經過Application.backgroundLoadingPriority
設定
在其餘因素相同的狀況下,異步加載方法的調用到加載對象可用之間最小有一幀延遲,致使異步加載方法比同步方法執行所需時間更長
根據運行環境可使用兩個不一樣的 API 自動追蹤 AssetBundle 之間的依賴。Editor 環境下,可以使用AssetDatabase
查詢依賴,使用AssetImporter
訪問和修改 AssetBundle 的分配和依賴;運行時,能夠經過基於 ScriptableObject 的 AssetBundleManifest
API 加載在 AssetBundle 構建期間生成的依賴項信息
當一個對象所在的 AssetBundle 被加載時,該對象就被分配了一個惟一的有效實例 ID,所以 AssetBundle 的加載順序並不重要,重要的是在加載該對象自己以前,要優先把全部包含其依賴對象的 AssetBundle 加載好。Unity 不會自動加載子 AssetBundle,具體可詳見手冊,例:
AssetBundle 1 中的 Material A 依賴於 AssetBundle 2 中的 Texture B,若要正常加載,與 AssetBundle 1 和 2 的加載順序無關,但必定要保證加載 Material A 時,AssetBundle 2 已加載
在構建 AssetBundle 時,Unity 建立一個包含每個 AssetBundle 依賴信息的類型爲 AssetBundleManifest 的序列化對象,該文件存在一個與其餘 AssetBundle 在同一打包路徑下的單獨的 AssetBundle 中,且與父層文件夾名相同
有兩種 API 查詢依賴
因該 API 會生成字符串數組,因此應儘可能少用,且避免性能高峯時使用
官方建議,大部分場合下,在進入性能需求高的場景前,儘量多地加載對象,尤爲對於移動平臺這種,訪問本地存儲慢,加載卸載對象引發內存流失會觸發垃圾回收的平臺
不適當地卸載 AssetBundle 會致使對象缺失或者在內存中重複
調用 AssetBundle.Unload
可卸載 AssetBundle 的頭信息,傳入參數 true 或 false 決定是否同時卸載該包下全部已加載對象。由此誕生一個問題,當卸載了 AssetBundle 未卸載已加載對象時,此時這些對象與 AssetBundle 便失去關聯了,從新加載 AssetBundle 並從新加載同一對象時,只會產生一個新的關聯對象,而舊對象則沒法使用AssetBundle.Unload
卸載了,這就致使了內存中同時存在兩個同樣的對象
爲避免該現象發生,有兩種通用處理方法:
針對遺留的未卸載對象:
Resources.UnloadUnusedAssets
Resources.UnloadUnusedAssets
對於場景資源統一的項目,可將每一個帶有資源的場景分別打包,在展現加載界面時,卸載舊場景所在的 AssetBundle 及其對象同時加載新場景所在的 AssetBundle
可按加載時機區分將對象打包分組,如人物形象、 UI、 模型和紋理、等長期存在的內容,可分爲一包並在開始時加載,其餘內容依據所需時機分組
另外還可能出現的問題是,在 AssetBundle 卸載以後加載 AssetBundle 中的對象時,會出現對象缺失的問題。出現該問題大部分緣由爲 Unity 丟失又從新得到對圖形上下文的控制,如移動設備 App 掛起,PC 鎖定等場景
根據實際狀況選擇 AssetBundle 時隨項目打包,或後續經過網絡下載,通常移動平臺因爲初始安裝大小和下載限制,會選擇後續下載,而主機和電腦則隨項目打包
隨項目打包有兩個主要緣由:
對於 Android 平臺,若 APK 通過壓縮,AssetBundle.LoadFromFile()
將須要更多時間讀取 StreamingAssets,且不一樣版本的 Unity 所使用的存儲算法可能也不同。對於通過壓縮的 APK 可以使用UnityWebRequest.GetAssetBundle
將 AssetBundle 解壓並緩存,但該操做會佔用額外的緩存空間;或者能夠導出 Gradle 項目並修改 build 文件添加無壓縮選項,隨後便可使用AssetBundle.LoadFromFile()
並省去解壓過程
通常推薦使用 UnityWebRequest
下載 AssetBundle,若下載包爲 LZMA 壓縮,則緩存的爲未壓縮或使用 LZ4 重壓縮的內容,若緩存已滿,則 Unity 會刪除最近最少使用的 AssetBundle。
建議僅當現有 API 的內存消耗、緩存行爲、性能不能知足或必須執行特定平臺的代碼時才使用自定義下載系統,如:
Unity 內置的 AssetBundle 緩存系統用於緩存 UnityWebRequestAssetBundle.GetAssetBundle
下載的包,緩存僅以名稱做爲惟一標識。另外可經過重載方法可傳入版本號(開發者本身管理版本號),緩存系統會比對版本號,選擇匹配版本或下載新包
緩存系統可經過 Caching.expirationDelay 和 Caching.maximumAvailableDiskSpace 修改最小未使用過時時間和最大緩存空間,當緩存文件在過時時間內沒被打開過即被刪除,或緩存空間不足,則優先刪除最近最少打開的緩存
自定義下載器需考慮四點:
可以使用以下三種方式快速實現:
若應用不須要支持 HTTPS/SSL,那麼 WebClient 提供了最簡單的下載機制,可實現異步直接下載到本地位置而不用額外的內存分配。
若須要更多參數選項控制下載器,則可以使用 HttpWebRequest:
依據資源在項目功能塊的使用位置,如 UI、角色、環境和其餘在生命週期中常出現的內容等分包
該分包方式適用於製做 DLC,能夠只下載單個實體而無需下載無變化的資源,其關鍵點在於須要開發者清楚瞭解每一個打包的資源所要用到的時機和位置
該方式適用於針對多平臺分包,例如音頻文件的壓縮設置在 Windows 和 Mac OS 平臺同樣,另外因爲紋理壓縮格式和設置等改變頻率遠低於腳本和預設體,使用該分配方式可使 AssetBundle 兼容更多的 Unity 版本
併發內容分包可理解爲以關卡爲分組依據,將一個關卡內獨有的角色、紋理、音樂等須要在同一時機加載的內容分爲一包
如有一個未分配資產被多個不一樣 AssetBundle 中的已分配資產引用,則在構建 AssetBundle 時,該引用對象會被拷貝到每一個 AssetBundle 中,形成空間和內存浪費
可以使用 AssetDatabase.GetDependencies
定位全部指定對象的依賴,使用AssetImporter
查詢分配了任何特定對象的AssetBundle
幾種解決方案:
任何自動生成的圖集會被分配到其包含精靈所在的 AssetBundle,若精靈對象被分配到多個包,則圖集會被複制,所以需確保同一圖集的精靈對象被分配到同一 AssetBundle 中
因爲 Android 生態的碎片化,常常須要使用不一樣格式壓縮紋理。全部 Android 設備都支持 ETC1 壓縮格式,但該格式不支持透明通道。若是應用不須要支持 OpenGL ES 2,則解決問題的最簡單方法是使用 ETC2,全部支持 OpenGL ES 3 的 Android 設備都支持該方法。若是必須分爲兩種壓縮格式,則可以使用 AssetBundle Variants。
要使用 AssetBundle 變體,全部不適用 ETC1 的紋理必須獨立到一個只有紋理的 AssetBundle 中,並建立該包對應不一樣壓縮格式的變體,如 DXT5, PVRTC 和 ATITC,更改變體中紋理的 TextureImporter 設置到對應的壓縮格式。運行時,使用 SystemInfo.SupportsTextureFormat 檢測設備支持的壓縮格式,並選擇對應變體
AssetBundle Variants 的主要做用在於使 AssetBundle 隨運行時環境調整其內容配置,AssetBundle Variants 使不一樣 AssetBundle 中的不一樣對對象在加載時公用一個實例 ID,使其看起來爲同一個對象。
有兩個經典案例:
AssetBundle Variants 也有必定缺陷,主要是不一樣 Asset 會組成不一樣的變體,哪怕兩組 Asset 之間僅僅是導入設置不一樣。該缺陷增長了管理大型項目資源的複雜程度,當改變一個資產時,全部的變體都須要更新。可以使用規範的命名規則確認變體做用或使用代碼在構建 AssetBundle 時更改導入設置
WebGL 僅支持主線程解壓和加載 AssetBundle 且僅支持未壓縮和 LZ4 壓縮格式,爲避免形成性能問題,建議減少包體,也可考慮使用 gzip/brotli 壓縮 AssetBundle