本文參照unity官網上對於assetBundle的一系列講解,主要針對assetbundle的知識點做一個梳理筆記,也為後續的資源打包設計做一個基礎學習,本文的代碼和圖片均來自unity官網,詳情可以查看Unity的DOCUMENTATION。

一、什麼是AssetBundle

AssetBundle就像一個ZIP壓縮文件,裡面存儲著不同平台的特殊資源(models/texture/prefabs/materials/audio clip/scenes...), 這些資源都可以在運行時進行載入。具體的assetBundle中主要包含什麼?主要包含兩種互相關聯的東西:

  1. 磁碟上的文件:也就是assetbundle文檔,可以將其視為一個容器或者文件夾,其中包含兩類文件:序列化文件和資源文件,序列化文件就是資源在打包後對應的各個平台的序列化操作後的文件,資源文件主要是針對textures/audio等較大的文件打包的二進位文件,這類文件在載入的時候是在其他線程執行的(效率更高)。
  2. 就是實際的assetbundle對象了,可以通過代碼來進行資源載入,其中主體是各個資源在進行載入的時候的存儲路徑圖。用圖表示:

二、如何對資源進行分組便於打包AssetBundle

雖然可以對AssetBundle自由的進行規劃,但是在進行項目的資源管理的時候,還是有一些規劃建議可以採用:

  1. 依據邏輯實體進行分組

這種資源分類方式是依據資源的功能進行分類,例如 UI/角色/場景/code等具體各自功能規劃的部分來進行資源分組,可以將所有的textures/layout data都打入UI相關分類中,可以將所有的模型和動畫都打入角色相關的資源中,將場景相關的貼圖和模型都打入場景資源中。

採用邏輯實體分組,對於資源的下載更新更為有利,由於資源的分類,可以在進行資源更新的時候,只更新對應的資源,而無需更新冗餘的其他資源。

使用這種分類方式最合適的策略,就是將資源進行詳細的分類(when and where will be used)

2.類型分組

主要依據資源的類型來進行分組,這樣對於不同的應用平台都具有一定的適用性。比如對於audio文件的壓縮設置,在mac和windows上都是一致的,那麼可以將audio文件都歸類為一類文件,實現文件資源的復用(不同平台的打包設置),對於shaders而言,對於不同平台需要不同的編譯設置,那麼就需要分類處理。這類分類方法,對於在不同的版本中變動頻率較低的代碼文件和prefabs顯得更有優勢。

3.相互關聯的內容分組

這種策略的,就是將需要同時進行載入的資源都歸類為一個分組,例如將不同場景中的角色都依據場景來進行分組,這就要求單獨一個場景中的資源只能用於該場景,各個分組之間沒有互相關聯的關係。這種分類方式,對於資源的載入時間有較大的縮減,這種分類方式的使用場合主要在場景資源中,在不同的場景資源中,其包含的資源各自互相不關聯。

在一個項目中,可以將上述的幾種策略都交互使用,對應具體的應用需求來靈活的採用分組策略,當然unity也提供了一些資源分組的tips:

  • 分離高頻和低頻更新的資源;
  • 將需要同時下載的資源合併進一個組,例如Model以及其關聯的animations;
  • 如果出現多個bundle中的多個object都依賴於另一個完全不同的bundle,那麼將這些依賴關係都移動到一個單獨的bundle,這樣可以降低依賴關係的複雜度;多個bundle均依賴於另一個bundle中的資源,那麼將這些bundle以及其依賴的資源歸類到一個資源,這樣可以降低資源的重複率(避免一份資源被拷貝到多個不同的bundle中);
  • 不可能同時載入的資源,需要歸類的各自的assetbundle中,例如標準和高配的資源;
  • 如果一個assetbundle中資源在載入的時候低於50%需要被載入,那麼可以考慮將這些需要被載入的資源單獨分類為一個資源(避免冗餘的載入);
  • 如果一組Objects對應的是一個資源的不同版本,那麼可以考慮assetbundle variants

三、如何打包AssetBundle

unity資源打包的介面,就是BuildPipeline.BuildAssetBundles函數,其對應具體的三個參數,第一個是bundle的輸出路徑,下面來詳細分析一下剩下的2個參數:

  1. BuildAssetBundleOptions

具體的參數設置,可以參看API當中的詳細信息,下面主要集中說三個參數,分別對應三種壓縮格式的選擇。

1)BuildAssetBundleOptions.None :

採用LZMA的壓縮格式, 這種壓縮格式要求資源在使用之前需要全部被解壓,這就會帶來在使用一個極小的文件的時候會額外帶來較長的解壓時間消耗。比較蛋疼的是,一旦這個bundle被解壓之後,在磁碟上又會以LZ4的格式重新壓縮,LZ4的壓縮格式,在使用資源的時候不需要全部解壓。

這種壓縮格式主要用於一個bundle中資源都需要被載入的時候,例如打包角色或者場景資源的時候,這種壓縮格式在初始化下載的時候被推薦(更小的包體),這些資源在被解壓後,又會以LZ4的格式被緩存。

2) BuildAssetBundleOptions.UncompressedAssetBundle:

無壓縮的打包,載入的文件更大,但是時間更快(省去解壓的時間)

3)BuildAssetBundleOptions.ChunkBasedCompression:

採用LZ4的壓縮格式,相比於LZMA而言文件體積更大,但是不要求在使用之前整個bundle都被解壓。LZ4使用chunk based 演算法,這就運行文件以chunk或者piece的方式載入,只解壓一個chunk文件,而無需解壓bundle中其餘不相關的chunk。

2. BuildTarget

也就是當前資源需要被使用的平台的分類,在打完資源後,會發現文件夾中有更多的文件,一般是2*(n+1), 對於各個不同的資源,都會有一個manifest文件,一般是 bundlename+".manifest",除此之外,還會有一個額外的manifest文件,對應不同的平台會有不同的額外的manifest(這是一個總體的manifest文件)。

對於manifest中內容,可以在文本文件中打開,一般舉例:

ManifestFileVersion: 0
CRC: 2422268106
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: 8b6db55a2344f068cf8a9be0a662ba15
TypeTreeHash:
serializedVersion: 2
Hash: 37ad974993dbaa77485dd2a0c38f347a
HashAppended: 0
ClassTypes:
- Class: 91
Script: {instanceID: 0}
Assets:
Asset_0: Assets/Mecanim/StateMachine.controller
Dependencies: {}

首先是版本,然後是CRC校驗碼,hash碼,classtypes,包含的資源及其路徑,依賴關係(dependencies)。

PS:

對於依賴關係就多詳細講解一下,所謂的依賴關係,指的是一個bundle中的一個或者多個UnityEngine.Objects包含對另一個bundle中的一個UnityEngine.Object的引用,如果一個bundle中的object對包含對其他非該bundle的object的引用,但是該object不屬於任何一個bundle,那麼這種依賴關係就不存在(這兒會帶來一個問題,如果多個bundle均引用一個不屬於bundle的object,那麼在載入的時候,各個bundle均會將該object進行copy到其內存中,這就帶來內存的額外冗餘佔用),所以依賴關係就是兩個或者多個bundle之間的object的引用關係。

對於依賴資源的載入,需要在該資源被載入前載入,unity不會自動的載入依賴資源,這需要在代碼中實現依賴資源的載入。

四、如何使用AssetBundle

在說完bundle的分類,打包後,接下來就是如何在實際的遊戲中載入使用這些bundle。在unity5以後,提供了4種不同類型的載入介面,下面逐一分析一下這四種不同介面的使用:

  1. AssetBundle.LoadFromMemoryAsync(byte[] binary, unit crc = 0)

這個方法用來載入ab數據的bytes數組,如果數據是使用LZMA的壓縮格式,那麼在載入的時候會進行解壓的操作,LZ4格式的數據則會保持其壓縮的狀態,使用示例:

using UnityEngine;
using System.Collections;
using System.IO;

public class Example : MonoBehaviour
{
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);
}
}

當然,對於bytes數組,也可以使用File.ReadAllBytes(path)的方式來載入數組。

2. AssetBundle.LoadFromFile

在載入非壓縮文件或者LZ4壓縮類型文件的時候,該介面效率極高,對於LZMA壓縮格式的文件,也會在載入的時候執行解壓的操作,使用示例:

public class LoadFromFileExample extends 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);
}
}

ps: 在unity5.3及更早的版本中,在安卓平台上如果從streaming assets路徑中載入文件會失敗(路徑文件夾中會額外包含.jar文件)。

3.WWW.LoadFromCacheOrDownload

這個介面會被淘汰(被UnityWebRequest替換),那麼就不過多的講解這個介面(注意這個介面會進行存儲分配的操作以容納資源,如果分配不足以存儲會使得載入失敗)。

4.UnityWebRequest

這個介面,會有兩步操作,首先是創建一個web request(調用UnityWebRequest.GetAssetBundle), 然後進行資源的獲取(調用DownloadHandlerAssetBundle.GetContent),unity提供的使用示例為:

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);
}

使用這種方式,可以使得開發者更為靈活的操作下載數據,同時進行內存使用分配,會逐漸的被用來WWW介面。

在載入完assetBundle後,接下來,就是如何從bundle中獲取資源(asset),其基本的介面模板為:

T objectFromBundle = bundleObject.LoadAsset<T>(assetName);

如果想獲取所有的assets則可以使用介面:

Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();

一旦獲取到asset,那麼就可以在遊戲中使用這些資源了(一般是實例化創建操作)。

5.載入AssetBundle Manifest

除了載入assetbundle,一般還會載入其對應的manifest(與其存儲在同一個文件夾下的相同名字的manifest),一般載入manifest的操作示例:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

在前文也提及到,如果一個assetbundle依賴於另一個assetbundle,那麼需要提前載入依賴相關的bundle,那麼依據manifest,可以載入其依賴的assetbundle:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); //Pass the name of the bundle you want the dependencies for.
foreach(string dependency in dependencies)
{
AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
}

現在,已經載入了assetbundle, 也獲取了assetbundle的dependencies,以及其中assets,這樣就可以管理這些assetbundle了。

五、管理AssetBundle

unity在場景中的Object被移除的時候不自動釋放objects,資源的清理需要再特定的時間觸發(場景切換)或者手動的管理。所以怎麼載入和卸載資源顯得尤為重要,不合適的載入可能會導致資源的重複載入,不合適的卸載可能會帶來資源的缺失(比如丟失貼圖)。

對於assetbundle的資源管理,最重要的是掌握什麼時候調用AssetBundle.Unload(bool)這個函數,傳入true/false會有不同的卸載策略。這個API會卸載對應的assetbundle的頭部信息,參數對應著是否同時卸載從該assetbundle中實例化的所有Objects。

AssetBundle.Unload(true)會卸載assetbundle中的所有gameobjects以及其依賴關係,但是並不包括基於其Objects實例化(複製)的Object(因為這些object不屬於該assetbundle,只是引用),所以當卸載貼圖相關的assetbundle的時候,場景中對其引用的實例化物體上會出現貼圖丟失,也就是場景中會出現紅色的區域,unity都會將其處理成貼圖丟失。

舉例說明,假設材質M來自於assetbundle AB, 如果 AB.Unload(true), 那麼場景中任何M的實例都會被卸載和銷毀,如果AB.Unload(false), 那麼就會切斷材質M實例與AB之間的關係:

那麼如果該assetbundle AB在後面再次被載入,unity不會重新關聯其關係,這樣在後續的使用中,就會出現一份材質的多個實例:

所以通常情況下,AssetBundle.Unload(false) 並不能帶來較為合理的釋放結果,AssetBundle.Unload(true)通常用來確保不會在內存中多次拷貝同一個資源,所以其更多的被項目所採納,此外還有兩個常用的方法用來確保其使用:

1)在遊戲中,對於場景的卸載有明確的規劃,比如在場景切換中或者場景載入中;

2)管理好對每個單獨的object的計數,只有在沒有引用的時候才卸載該assetbundle,這樣可以規避載入和卸載過程中的多份內存拷貝問題。

如果要使用AssetBundle.Unload(false), 那麼這些實例化的對象可以通過2中途徑卸載:

1)清除對不需要物體的所有引用,場景和代碼中都需要清楚,然後調用Resources.UnloadUnusedAssets;

2) 在場景載入的時候採用非增量的方式載入,這會清楚當前場景中的所有Objects,然後反射自動調用Resources.UnloadUnusedAssets

如果你不想管理這些assetbundle,unity推出了AssetBundle Manager,可以學習了解一下,此外Unity還推出了一些AssetBundle Browser Tool, 也可以學習了解一下。

推薦閱讀:

相关文章