Unity學習—AssetBundle

本篇文章主要內容來自於官方教程 Assets, Resources and AssetBundles,介紹了 AssetBundle 的各種機制,使用方式和適用場景等html

有關其餘 Unity 資源管理的內容可見Unity學習—資源管理概覽git

文中全部 API 均以版本 2019.3 爲準github

本文原地址:Unity學習—AssetBundle算法

AssetBundle 做用

AssetBundle 是外部資產的集合,可獨立於 Unity 構建過程外,是 Unity 更新非代碼內容的主要工具,常常置於服務器上供用戶終端動態獲取;AssetBundle 使開發者能夠提交更小的應用包,最小化運行時內存壓力,使終端能夠選擇性加載優化內容c#

AssetBundle 組成

AssetBundle 主要由兩部分組成:文件頭和數據段數組

文件頭包含了id、壓縮類型、索引清單,該索引清單是與 Resources 相同的記錄了序列化文件中的字節偏移量的查找表。對於大部分平臺該表爲平衡搜索樹,對 Windows 和 OSX 系列(包括 iOS)則爲紅黑樹,隨着 AssetBundle 中對象的增長,構造清單所需時間的增加速度將超過線形增加速度緩存

數據段包含了 Asset 通過序列化的原始數據,數據還可選擇是否壓縮,若使用 LZMA 壓縮,則將全部 Asset 的字節數組總體壓縮;若使用 LZ4 壓縮,則將每一個 Asset 單獨壓縮;若不壓縮,則數據保持原始字節流服務器

AssetBundle 加載

有四種不一樣的 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

AssetBundle Asset 加載

同步異步加載 Asset 一共有六種 API 可以使用,同步方法必定比對應的異步方法快至少一幀

  • LoadAsset (LoadAssetAsync)
  • LoadAllAssets (LoadAllAssetsAsync)
  • LoadAssetWithSubAssets (LoadAssetWithSubAssetsAsync)

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設定

  • ThreadPriority.High: 最大 50 毫秒每幀
  • ThreadPriority.Normal: 最大 10 毫秒每幀
  • ThreadPriority.BelowNormal: 最大 4 毫秒每幀
  • ThreadPriority.Low: 最大 2 毫秒每幀

在其餘因素相同的狀況下,異步加載方法的調用到加載對象可用之間最小有一幀延遲,致使異步加載方法比同步方法執行所需時間更長

AssetBundle 依賴

根據運行環境可使用兩個不一樣的 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 查詢依賴

  • AssetBundleManifest.GetAllDependencies 獲取 AssetBundle 的全部依賴層級
  • AssetBundleManifest.GetDirectDependencies 獲取 AssetBundle 直接依賴

因該 API 會生成字符串數組,因此應儘可能少用,且避免性能高峯時使用

官方建議,大部分場合下,在進入性能需求高的場景前,儘量多地加載對象,尤爲對於移動平臺這種,訪問本地存儲慢,加載卸載對象引發內存流失會觸發垃圾回收的平臺

AssetBundle 使用

不適當地卸載 AssetBundle 會致使對象缺失或者在內存中重複

調用 AssetBundle.Unload可卸載 AssetBundle 的頭信息,傳入參數 true 或 false 決定是否同時卸載該包下全部已加載對象。由此誕生一個問題,當卸載了 AssetBundle 未卸載已加載對象時,此時這些對象與 AssetBundle 便失去關聯了,從新加載 AssetBundle 並從新加載同一對象時,只會產生一個新的關聯對象,而舊對象則沒法使用AssetBundle.Unload卸載了,這就致使了內存中同時存在兩個同樣的對象

img

爲避免該現象發生,有兩種通用處理方法:

  1. 最便捷經常使用的方法是在應用生命週期特定點,如關卡切換或加載界面時,卸載 AssetBundle
  2. 對每一個對象引用計數,僅在全部 AssetBundle 對象未使用時卸載

針對遺留的未卸載對象:

  1. 銷燬場景和代碼中全部該對象的引用後,調用Resources.UnloadUnusedAssets
  2. 切換到一個非疊加型的新場景會銷燬當前場景全部對象並自動調用Resources.UnloadUnusedAssets

對於場景資源統一的項目,可將每一個帶有資源的場景分別打包,在展現加載界面時,卸載舊場景所在的 AssetBundle 及其對象同時加載新場景所在的 AssetBundle

可按加載時機區分將對象打包分組,如人物形象、 UI、 模型和紋理、等長期存在的內容,可分爲一包並在開始時加載,其餘內容依據所需時機分組

另外還可能出現的問題是,在 AssetBundle 卸載以後加載 AssetBundle 中的對象時,會出現對象缺失的問題。出現該問題大部分緣由爲 Unity 丟失又從新得到對圖形上下文的控制,如移動設備 App 掛起,PC 鎖定等場景

AssetBundle 發佈

根據實際狀況選擇 AssetBundle 時隨項目打包,或後續經過網絡下載,通常移動平臺因爲初始安裝大小和下載限制,會選擇後續下載,而主機和電腦則隨項目打包

隨項目打包有兩個主要緣由:

  1. 減小項目構建時長,簡化迭代開發,針對無需單獨更新的 AssetBundle 可放在 StreamingAssets 目錄下
  2. 發佈可更新的初始修正內容,用於節省用戶初始安裝後的時間和爲後續修復作準備。但 StreamingAssets 不適用於該狀況,若不考慮自定義下載和緩存系統,則可使用 Unity 的緩存系統,從 StreamingAssets 下載初始緩存

對於 Android 平臺,若 APK 通過壓縮,AssetBundle.LoadFromFile()將須要更多時間讀取 StreamingAssets,且不一樣版本的 Unity 所使用的存儲算法可能也不同。對於通過壓縮的 APK 可以使用UnityWebRequest.GetAssetBundle 將 AssetBundle 解壓並緩存,但該操做會佔用額外的緩存空間;或者能夠導出 Gradle 項目並修改 build 文件添加無壓縮選項,隨後便可使用AssetBundle.LoadFromFile()並省去解壓過程

通常推薦使用 UnityWebRequest下載 AssetBundle,若下載包爲 LZMA 壓縮,則緩存的爲未壓縮或使用 LZ4 重壓縮的內容,若緩存已滿,則 Unity 會刪除最近最少使用的 AssetBundle。

建議僅當現有 API 的內存消耗、緩存行爲、性能不能知足或必須執行特定平臺的代碼時才使用自定義下載系統,如:

  • 須要控制緩存細粒度時
  • 須要自定義緩存策略時
  • 須要執行平臺特定代碼,如 iOS 的後臺下載
  • 須要在不支持 SSL 的品臺上使用 SSL 下載

Unity 內置的 AssetBundle 緩存系統用於緩存 UnityWebRequestAssetBundle.GetAssetBundle下載的包,緩存僅以名稱做爲惟一標識。另外可經過重載方法可傳入版本號(開發者本身管理版本號),緩存系統會比對版本號,選擇匹配版本或下載新包

緩存系統可經過 Caching.expirationDelayCaching.maximumAvailableDiskSpace 修改最小未使用過時時間和最大緩存空間,當緩存文件在過時時間內沒被打開過即被刪除,或緩存空間不足,則優先刪除最近最少打開的緩存

自定義下載器

自定義下載器需考慮四點:

  • 下載機制
  • 存儲位置
  • 壓縮類型
  • 補丁

可以使用以下三種方式快速實現:

  • C# 提供的 HttpWebRequest 和 WebClient 類
  • 自定義原生插件
  • 資源商店包

若應用不須要支持 HTTPS/SSL,那麼 WebClient 提供了最簡單的下載機制,可實現異步直接下載到本地位置而不用額外的內存分配。

若須要更多參數選項控制下載器,則可以使用 HttpWebRequest:

  • 經過 HttpWebResponse.GetResponseStream 獲取字節流
  • 在棧上分配固定大小的字節緩衝區
  • 讀取請求結果放入緩衝區
  • 使用 IO 將緩衝區數據寫入磁盤

Asset 分包策略

  • 邏輯實體分包
  • 對象類型分包
  • 併發內容分包

邏輯實體分包

依據資源在項目功能塊的使用位置,如 UI、角色、環境和其餘在生命週期中常出現的內容等分包

  • 將全部 UI 的紋理和佈局數據分包
  • 將角色的模型和動畫數據分包
  • 將多場景共用的紋理和模型分包

該分包方式適用於製做 DLC,能夠只下載單個實體而無需下載無變化的資源,其關鍵點在於須要開發者清楚瞭解每一個打包的資源所要用到的時機和位置

對象類型分包

該方式適用於針對多平臺分包,例如音頻文件的壓縮設置在 Windows 和 Mac OS 平臺同樣,另外因爲紋理壓縮格式和設置等改變頻率遠低於腳本和預設體,使用該分配方式可使 AssetBundle 兼容更多的 Unity 版本

併發內容分包

併發內容分包可理解爲以關卡爲分組依據,將一個關卡內獨有的角色、紋理、音樂等須要在同一時機加載的內容分爲一包

Tips

  • 將常更新與不常更新內容分開
  • 將須要同時加載的對象分爲一組,如一個模型,其所需的材質和動畫分爲一組
  • 若多個 AssetBundle 中的多個對象引用了其餘 AssetBundle 中的單個 Asset,則將依賴項分離到單獨的包中以減小重複
  • 確保兩組徹底不可能同時加載的對象不在用一包中,如低清和高清材質包
  • 若一個包中只有低於一半的對象被頻繁加載,可將其拆分
  • 將一些同時加載的小包(資源少於5到10個)合併
  • 若一個包中的對象僅是版本不一樣,則可使用 AssetBundle 變體

常見問題

資產重複

如有一個未分配資產被多個不一樣 AssetBundle 中的已分配資產引用,則在構建 AssetBundle 時,該引用對象會被拷貝到每一個 AssetBundle 中,形成空間和內存浪費

可以使用 AssetDatabase.GetDependencies定位全部指定對象的依賴,使用AssetImporter查詢分配了任何特定對象的AssetBundle

幾種解決方案:

  • 確保不一樣包中的對象沒有共用依賴,或有共用依賴的對象分到一個包中,但對部分項目該方法會使 AssetBundle 過大不便於重複構建和下載
  • 將有共同依賴項的 AssetBundle 分段,使其不會同時加載
  • 使全部依賴項被分配到 AssetBundle,但會增長應用追蹤依賴的複雜度

精靈圖集重複

任何自動生成的圖集會被分配到其包含精靈所在的 AssetBundle,若精靈對象被分配到多個包,則圖集會被複制,所以需確保同一圖集的精靈對象被分配到同一 AssetBundle 中

Android 紋理

因爲 Android 生態的碎片化,常常須要使用不一樣格式壓縮紋理。全部 Android 設備都支持 ETC1 壓縮格式,但該格式不支持透明通道。若是應用不須要支持 OpenGL ES 2,則解決問題的最簡單方法是使用 ETC2,全部支持 OpenGL ES 3 的 Android 設備都支持該方法。若是必須分爲兩種壓縮格式,則可以使用 AssetBundle Variants。

要使用 AssetBundle 變體,全部不適用 ETC1 的紋理必須獨立到一個只有紋理的 AssetBundle 中,並建立該包對應不一樣壓縮格式的變體,如 DXT5, PVRTC 和 ATITC,更改變體中紋理的 TextureImporter 設置到對應的壓縮格式。運行時,使用 SystemInfo.SupportsTextureFormat 檢測設備支持的壓縮格式,並選擇對應變體

AssetBundle 變體

AssetBundle Variants 的主要做用在於使 AssetBundle 隨運行時環境調整其內容配置,AssetBundle Variants 使不一樣 AssetBundle 中的不一樣對對象在加載時公用一個實例 ID,使其看起來爲同一個對象。

有兩個經典案例:

  • 變體簡化了 AssetBundle 對特定平臺的加載過程,例如高清和低清平臺使用不一樣的變體,代碼上只用同一個對象名稱便可加載合適的資源
  • 變體使應用能夠針對同一平臺的不一樣硬件加載不一樣內容,如移動平臺不一樣機型之間的顯示紋理不一樣

AssetBundle Variants 也有必定缺陷,主要是不一樣 Asset 會組成不一樣的變體,哪怕兩組 Asset 之間僅僅是導入設置不一樣。該缺陷增長了管理大型項目資源的複雜程度,當改變一個資產時,全部的變體都須要更新。可以使用規範的命名規則確認變體做用或使用代碼在構建 AssetBundle 時更改導入設置

其餘

是否壓縮

  • 未壓縮的 AssetBundle 加載速度比壓縮過的快不少
  • 在構建 AssetBundle 時壓縮很耗時
  • 壓縮包占用空間小
  • 未壓縮或 LZ4 佔用更少內存
  • 包體過大或平臺可用網絡帶寬太小建議壓縮,不然不壓縮
  • 主要由使用 Crunch 壓縮算法的 DXT 壓縮紋理組成的包應不壓縮

WebGL

WebGL 僅支持主線程解壓和加載 AssetBundle 且僅支持未壓縮和 LZ4 壓縮格式,爲避免形成性能問題,建議減少包體,也可考慮使用 gzip/brotli 壓縮 AssetBundle

參考

Unity資源管理

相關文章
相關標籤/搜索