AssetBundle详解

AssetBundle详解

5 年前 · 来自专栏 Unity3D从相爱到相离

一:AssetBundle介绍

二:AssetBundle多平台打包

三:AssetBundle资源加载和卸载

四:AssetBundle服务器下载

五:AssetBundle原理分析

六:AssetBundle依赖加载


一:AssetBundle介绍

AssetBundle是将资源使用Unity提供的一种用于存储资源的压缩格式打包后的集合,它可以存储任何一种Unity可以识别的资源,如模型,纹理图,音频,场景等资源。也可以加载开发者自定义的二进制文件。他们的文件类型是.assetbundle/.unity3d,他们先前被设计好,很容易就下载到我们的游戏或者场景当中。

一般情况下AssetBundle的具体开发流程如下:

(1)创建Asset bundle,开发者在unity编辑器中通过脚本将所需要的资源打包成AssetBundle文件。

(2)上传服务器。开发者将打包好的AssetBundle文件上传至服务器中。使得游戏客户端能够获取当前的资源,进行游戏的更新。

(3)下载AssetBundle,首先将其下载到本地设备中,然后再通过AsstBudle的加载模块将资源加到游戏之中。

(4)加载,通过Unity提供的API可以加载资源里面包含的模型、纹理图、音频、动画、场景等来更新游戏客户端。

(5)卸载AssetBundle,卸载之后可以节省内存资源,并且要保证资源的正常更新。

二:AssetBundle多平台打包

2.1创建AssetBundle

(1)只有在Asset窗口中的资源才可以打包,我们单击GameObject->Cube,然后在Asset窗口创建一个预设体,命名为cubeasset,讲Cube拖到该预设体上。

(2)单击刚创建的预制件cubeasset,在编辑器界面右下角的属性窗口底部有一个名为”AssetBundle”的创建工具。接下来创建即可,空的可以通过单击菜单选项”New....”来创建,将其命名为”cubebundle”,名称固定为小写,如果使用了大写字母之后,系统会自动转换为小写格式。

2.2 打包AssetBundle

AssetBundle创建之后需要导出,这一个过程就需要编写相应的代码实现,从Unity5.x之后,提供了一套全新简单的API来实现打包功能。大大简化了开发者手动遍历资源自行打包的过程,更加方便快捷。需要在Asset目录下创建Editor目录,表示该脚本是对于编辑器的一个扩展。脚本写完之后,也不需要进行挂载,会自动在Unity的菜单栏中生成。单击子菜单,既可以进行打包AssetBundle。具体实现代码如下:

using UnityEditor;
public class ExportAssets : MonoBehaviour {
	[@MenuItem("Test/Build Asset Bundles")]
	static void BuildAssetBundles(){
BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath,BuildAssetBundleOptions.UncompressedAssetBundle,BuildTarget.StandaloneOSXUniversal);

所有的AssetBundle已经被导出,此时每一个AssetBundle资源会有一个和文件相关的Mainfest 的文本类型的文件,该文件提供了所打包资源的CRC和资源依赖的信息。

除此之外,还有一个AssetBundle文件会在生成的时候被创建,记录者整个资源列表以及列表之间的关系。

2.3. AssetBundle的压缩类型

Unity3D引擎为我们提供了三种压缩策略来处理AssetBundle的压缩,即:

  • LZMA格式
  • LZ4格式
  • 不压缩

LZMA格式:

在默认情况下,打包生成的AssetBundle都会被压缩。在U3D中,AssetBundle的标准压缩格式便是LZMA(LZMA是一种序列化流文件),因此在默认情况下,打出的AssetBundle包处于LZMA格式的压缩状态,在使用AssetBundle前需要先解压缩。

使用LZMA格式压缩的AssetBundle的包体积最小(高压缩比),但是相应的会增加解压缩时的时间。

LZ4格式:

Unity 5.3之后的版本增加了LZ4格式压缩,由于LZ4的压缩比一般,因此经过压缩后的AssetBundle包体的体积较大(该算法基于chunk)。但是,使用LZ4格式的好处在于解压缩的时间相对要短。

若要使用LZ4格式压缩,只需要在打包的时候开启 BuildAssetBundleOptions.ChunkBasedCompression即可。

BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath,

BuildAssetBundleOptions.ChunkBasedCompression);

不压缩:

当然,我们也可以不对AssetBundle进行压缩。没有经过压缩的包体积最大,但是访问速度最快。

若要使用不压缩的策略,只需要在打包的时候开启BuildAssetBundleOptions.UncompressedAssetBundle即可。

BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath,

BuildAssetBundleOptions.UncompressedAssetBundle);

static void BuildAssetBundlesAndroid(){
		Object obj = AssetDatabase.LoadMainAssetAtPath("Assets/Test.png");
		BuildPipeline.BuildAssetBundle(obj, null,
		                               Application.streamingAssetsPath + "/Test.assetbundle",
		         BuildAssetBundleOptions.CollectDependencies |        BuildAssetBundleOptions.CompleteAssets
		        | BuildAssetBundleOptions.DeterministicAssetBundle, BuildTarget.StandaloneWindows);

三:AssetBundle资源加载和卸载

3.1 AssetBundle加载

在5.3版本中的新AssetBundle系统中,旧有的一些动态加载API已经被新的API所取代,具体内容如下:

4.x-5.2版本中的AssetBundle.CreateFromFile方法,在5.3版本中变成了AssetBundle.LoadFromFile方法。

4.x-5.2版本中的AssetBundle.CreateFromMemory方法,在5.3版本中变成了LoadFromMemoryAsync方法。

4.x-5.2版本中的AssetBundle.CreateFromMemoryImmediate方法,在5.3版本中变成了LoadFromMemory方法。

因此,本小节之后的内容将使用新版API。

使用AssetBundle动态加载资源首先要获取AssetBundle对象,第二步才是从AssetBundle中加载目标资源。因此本小节将主要关注如何在运行时获取AssetBundle的对象,关于如何从AssetBundle中加载资源将在下一小节中分析。

要在运行时加载AssetBundle对象主要可以分为两大类途径:

  • 先获取WWW对象,再通过WWW.assetBundle获取AssetBundle对象
  • 直接获取AssetBundle

下面我们就具体分析一下这两种途径:

(1)先获取WWW对象,再通过WWW.assetBundle加载AssetBundle对象:

在先获取WWW对象,在获取AssetBundle的这种方式中,我们可以使用以下两个API来实现这个功能。

  • public WWW(string url),直接调用WWW类的构造函数,目标AssetBundle所在的路径作为其参数,构造WWW对象的过程中会加载Bundle文件并返回一个WWW对象,完成后会在内存中创建较大的WebStream(解压后的内容,通常为原Bundle文件的4~5倍大小,纹理资源比例可能更大),因此后续的AssetBundle.LoadAsset可以直接在内存中进行。
  • public static WWW LoadFromCacheOrDownload(string url, int version, uint crc = 0),WWW类的一个静态方法,调用该方法同样会加载Bundle文件同时返回一个WWW对象,和上一个直接调用WWW的构造函数的区别在于该方法会将解压形式的Bundle内容存入磁盘中作为缓存(如果该Bundle已在缓存中,则省去这一步),完成后只会在内存中创建较小的SerializedFile,而后续的AssetBundle.LoadAsset需要通过IO从磁盘中的缓存获取。

● public static AssetBundle CreateFromFile(string path);

通过未压缩的Bundle文件,同步创建AssetBundle对象,这是最快的创建方式。创建完成后只会在内存中创建较小的SerializedFile,而后续的AssetBundle.Load需要通过IO从磁盘中获取。加载时候选择未压缩即可,Unity2017新版本也可以加载压缩的AssetBundle.

● 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 bundle= AssetBundle.LoadFromFileApplication.streamingAssetsPath+"/cube.assetbundle");
Material mat = (Material)bundle.LoadAsset ("Red");
GetComponent<MeshRenderer> ().material = mat;
    IEnumerator GetData(){
		WWW data = new WWW ("file://"+Application.streamingAssetsPath+
		                    "/cube.assetbundle");
		yield return data;
		Material mat = (Material)data.assetBundle.LoadAsset ("Red");
		GetComponent<MeshRenderer> ().material = mat;
    Hash128 hash = new Hash128 ();
	WWW data=WWW.LoadFromCacheOrDownload("file://"+Application.streamingAssetsPath+ "/cube.assetbundle",new Hash128(1,1,1,1));
		yield return data;
		Material mat = (Material)data.assetBundle.LoadAsset ("Red");
		GetComponent<MeshRenderer> ().material = mat;

若使用www加载本地AssetBundle则需要在路径前加“file://”即:

"file://" + Application.dataPath + "/ASB/b.unity3d"


IEnumerator downloadTexture(string url){
		WWW data = new WWW (url);
		yield return data;
		AssetBundle bundle = AssetBundle.CreateFromMemoryImmediate 
			(data.bytes);
		sphere.GetComponent<Renderer> ().material.mainTexture
			= (Texture)bundle.LoadAsset ("01");
		bundle.Unload (false);

接口对比:new WWW与WWW.LoadFromCacheOrDownload

前者的优势

● 后续的Load操作在内存中进行,相比后者的IO操作开销更小;

● 不形成缓存文件,而后者则需要额外的磁盘空间存放缓存;

● 能通过WWW.texture,WWW.bytes,WWW.audioClip等接口直接加载外部资源,而后者只能用于加载AssetBundle;

前者的劣势

● 每次加载都涉及到解压操作,而后者在第二次加载时就省去了解压的开销;

● 在内存中会有较大的WebStream,而后者在内存中只有通常较小的SerializedFile。(此项为一般情况,但并不绝对,对于序列化信息较多的Prefab,很可能出现SerializedFile比WebStream更大的情况)

我们自己压缩的AssetBundle:

我们自己也可以使用第三方库或工具对生成的AssetBundle包文件进行压缩,如果需要这样做,则我们最好不要再使用Unity3D对AssetBundle进行压缩,因此在打包时选择开启BuildAssetBundleOptions.UncompressedAssetBundle。

在运行时需要加载AssetBundle对象时,使用LoadFromFileAsync方法进行异步加载。

3.2 AssetBundle内部资源加载

新版的AssetBundle中,加载资源的API已经变成了以下的几个:

  • LoadAsset:从资源包中加载指定的资源
  • LoadAllAsset:加载当前资源包中所有的资源
  • LoadAssetAsync:从资源包中异步加载资源
                //1 同步加载,根据名称进行加载

		Texture mat = (Texture)data.assetBundle.LoadAsset ("1");

		GetComponent<MeshRenderer> ().material.mainTexture = mat;

		data.assetBundle.Unload (false);

		//2同步加载所有的GameObject类型

		GameObject[] obj = data.assetBundle.LoadAllAssets<GameObject> ();

		//3Async加载,根据名称进行加载

		AssetBundleRequest req= data.assetBundle.LoadAssetAsync("1");

		yield return req;

		if (req.isDone) {

			Texture tex = (Texture)req.asset;

		//4 LoadAllAssets的yibu版本

		AssetBundleRequest req1=data.assetBundle.LoadAllAssetsAsync

			<GameObject>();


从AssetBundle加载资源的常用API

  • public Object LoadAsset(string name, Type type);

通过给定的名字和资源类型,加载资源。加载时会自动加载其依赖的资源,即Load一个Prefab时,会自动Load其引用的Texture资源。

  • public Object[] LoadAllAsset(Type type);

一次性加载Bundle中给定资源类型的所有资源。

  • public AssetBundleRequest LoadAssetAsync(string name, Type type);

该接口是Load的异步版本。

注:5.x下分别改名为LoadAsset,LoadAllAssets,LoadAssetAsync,并增加了LoadAllAssetsAsync。

资源卸载

资源卸载部分的变化不大,使用的仍然是Unload方法。

该方法会卸载运行时内存中包含在bundle中的所有资源。

当传入的参数为true,则不仅仅内存中的AssetBundle对象包含的资源会被销毁。根据这些资源实例化而来的游戏内的对象也会销毁。

当传入的参数为false,则仅仅销毁内存中的AssetBundle对象包含的资源。

四:AssetBundle服务器下载

我们采用下面的案例,场景进入的时候,依次从服务器给三个已经创建的对象加载纹理,材质,以及根据预设创建一个新的对象。

服务器我们简单的使用python进行搭建,进入到存放asset资源目录下,输入以下命令就可以搭建一个简单的文件服务器:

python -m SimpleHTTPServer 8080

浏览器输入 127.0.0.1:8080/ 可以查看搭建的情况。

具体过程如下:

(1)新建一个场景,创建立方体,球体和一个空对象,分别用来测试下载纹理,材质和预设体。

(2)分别将材质和纹理图片,预设体创建成为AssetBundle资源,具体的命名自己把握,后来需要根据名称进行获取。

(3)打包成功之后,将内容添加到对应的服务器之中,下面开始脚本的编写。

(4)新建脚本如下,分别测试三个内容的下载。

public class DownloadAsset : MonoBehaviour {
	public GameObject goCube; //演示修改纹理
	public GameObject goSphere;//演示修改材质
	public Transform newPosition;//演示根据预设体创建对象
	//url=IP+文件名
	private string url1="http://127.0.0.1:8080/image";
	private string url2="http://127.0.0.1:8080/mater";
	private string url3="http://127.0.0.1:8080/cubebundle";
	// Use this for initialization
	void Start () {
		//downTexture ();
		downMat ();
		//downPrefab ();
   void downTexture()
		StartCoroutine (download_Texture(url1));
	IEnumerator download_Texture(string url){
		WWW downAsset = new WWW (url);
		yield return downAsset;
		goCube.GetComponent<Renderer> ().material.mainTexture
			= (Texture)downAsset.assetBundle.LoadAsset ("01");
		downAsset.assetBundle.Unload (false);
	void downMat(){
		StartCoroutine (download_Mat(url2));
	IEnumerator download_Mat(string url){
		WWW downAsset = new WWW (url);
		yield return downAsset;
		goCube.GetComponent<Renderer> ().material
			= (Material)downAsset.assetBundle.LoadAsset 
			("Red");
		downAsset.assetBundle.Unload (false);
	void downPrefab(){
		StartCoroutine (download_Prefab(url3));
	IEnumerator download_Prefab(string path){
		WWW downloadAsset = new WWW (path);
		yield return downloadAsset;
		GameObject gpPrefabs = (GameObject)Instantiate
			(downloadAsset.assetBundle.LoadAsset("Cube"));
		gpPrefabs.GetComponent<Renderer> ().material.color = 
			Color.yellow;
		gpPrefabs.transform.position =
			newPosition.transform.position;
		downloadAsset.assetBundle.Unload (false);
		downloadAsset.Dispose ();

当然,我们这里也可以使用异步加载的方式加载AssetBundle的资源,如下所示:

		AssetBundle bundle = downloadAsset.assetBundle;
		AssetBundleRequest request = bundle.LoadAssetAsync 
			("Cube",typeof(GameObject));
		yield return request;
		GameObject gpPrefabs = (GameObject)Instantiate
			(request.asset);
		gpPrefabs.GetComponent<Renderer> ().material.color = Color.yellow;
		gpPrefabs.transform.position =newPosition.transform.position;
		downloadAsset.assetBundle.Unload (false);
		downloadAsset.Dispose ();

五:AssetBundle原理分析

5.1 AssetBundle加载

当AssetBundle 解压加载到内存之后,我们可以通过WWW.assetbundle属性获得AssetBundle对象(上图的粉色框部分)来得到各个Assets,并对这些Assets进行加载或者实例化操作。

在加载过程中,unity会将AssetBundle中的数据流转变成unity可识别的信息类型,如:材质、纹理等。加载完成之后,我们就可以对其进行更多操作了,如:对象的实例化、材质复用、纹理替换等等。

5.2 AssetBundle及Assets的卸载

在AssetBundle的下载和加载过程中,以及Assets加载和实例化过程中,AssetBundle以及加载的Assets都会占用内存。

(1)AssetBundle的卸载采用Assetbundle.Unload(bool)接口。

(2)Assets的卸载有两种方式:

①.Assetbundle.Unload(true)。这会强制卸载掉所有从AssetBundle加载的Assets。

②.Resource.UnloadUnusedAssets()和 Resources.UnloadAsset。这会卸载掉所有没有用到的Assets。需要注意的是,该接口作用于整个系统,而不仅仅是当前的AssetBundle,而且不会卸载从当前AssetBundle文件中加载并仍在使用的Assets。

(3)对于实例化出来的对象,可以使用GameObject.Destroy或GameObject.DestroyImmediate。注意的是:官方说法是这样的,如果使用GameObject.Destroy接口,unity会将真正的删除操作延后到一个合适的时机统一进行处理,但会在渲染之前。

WWW对象和WWW.asssetbundle加载的AssetBundle对象都会对Web Stream数据持有引用。AssetBundle对象同时也会引用到从它加载的所有Assets。按照官方说法,真正的数据都是存放在Web Stream数据中(如纹理、模型),而WWW和AssetBundle对象只是一个结构指向了Web Stream数据。

对于WWW对象,可以使用www=null或www.dispose来卸载。这两者是有区别的,www=null不会立即释放内存,而是系统的自动回收机制启动时回收。www.dispose则会立即调用系统的回收机制来释放内存。当WWW对象被释放后,其对于Web Stream数据的引用计数也会相应减1。

对于Web Stream数据,它所占用的内存会在其引用计数为0时,被系统自动回收。例如:当上图中的AssetBundle对象和WWW对象被释放后,Web Stream数据所占内存也会被系统自动回收。

六:AssetBundle依赖加载

如果一个或者多个 UnityEngine.Objects 引用了其他 AssetBundle 中的 UnityEngine.Object,那么 AssetBundle 之间就产生的依赖关系。相反,如果 UnityEngine.ObjectA 所引用的UnityEngine.ObjectB 不是其他 AssetBundle 中的,那么依赖就不会产生。

假若这样(指的是前面两个例子的后者,既不产生依赖的情况),被依赖对象(UnityEngine.ObjectB)将被拷贝进你创建的 AssetBundle(指包含 UnityEngine.ObjectA 的 AssetBundle)。

更进一步,如果有多个对象(UnityEngine.ObjectA1、UnityEngine.ObjectA2、UnityEngine.ObjectA3......)引用了同一个被依赖对象(UnityEngine.ObjectB),那么被依赖对象将被拷贝多份,打包进各个对象各自的 AssetBundle。

如果一个 AssetBundle 存在依赖性,那么要注意的是,那些包含了被依赖对象的 AssetBundles,需要在你想要实例化的对象的加载之前加载。Unity 不会自动帮你加载这些依赖。

想想看下面的例子, Bundle1 中的一个材质(Material)引用了 Bundle2 中的一个纹理(Texture):

在这个例子中,在从 Bundle1 中加载材质前,你需要先将 Bundle2 加载到内存中。你按照什么顺序加载 Bundle1 和 Bundle2 并不重要,重要的是,想从 Bundle1 中加载材质前,你需要先加载 Bundle2。

(1)AssetBundle.LoadFromMemoryAsync

这个方法的参数是一个包含了 AssetBundle 数据的字节数组。如果需要的话,你还可以传入一个 CRC(循环冗余校验码) 参数。如果 AssetBundle 使用 LZMA 算法压缩,那么 AssetBundle 在加载的时候会被解压。如果 AssetBundle 使用 LZ4 算法压缩,它将直接以压缩形式被加载。

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

这个 API 在加载本地存储的未压缩 AssetBundle 时具有很高效率。如果 AssetBundle 是未压缩,或者是数据块形式(LZ4 算法压缩)的,LoadFromFile 将从磁盘中直接加载它。如果 AssetBundle 是高度压缩(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);

注意:在安卓设备上,如果 Unity 是5.3或者更老的版本,这个方法在读取资源流路径(Streaming Assets path)的时候会失败。这是因为那个路径是在一个 .jar 文件的内部。Unity5.4 以及更高的版本没有这个问题,可以正常的读取资源流。

WWW.LoadFromCacheOrDownload

这个 API 已经被废弃(建议使用 UnityWebRequest)(三思:这句话不是我加的,官方文档中就是有这句话)

这个 API 对于从远程服务器加载 AssetBundles,或者加载本地 AssetBundles 都很有用。这个 API 是 UnityWebRequest 不尽如人意的老版本。

从远程服务器加载的 AssetBundle 将会被自动缓存。如果 AssetBundle 是压缩形式的,一个工作线程将加速解压这个 AssetBundle 并写入缓存。一旦一个 AssetBundle 已经被解压且被缓存,它将完全像使用 AssetBundle.LoadFromFile 方法一样被加载。

下面是一个如何使用这个方法的例子:

using UnityEngine;
using System.Collections;
public class LoadFromCacheOrDownloadExample : MonoBehaviour
    IEnumerator Start ()
            while (!Caching.ready)
                    yield return null;
        var www = WWW.LoadFromCacheOrDownload("http://myserver.com/myassetBundle", 5);
        yield return www;
        if(!string.IsNullOrEmpty(www.error))
            Debug.Log(www.error);
            yield return;
        var myLoadedAssetBundle = www.assetBundle;
        var asset = myLoadedAssetBundle.mainAsset;

由于缓存 AssetBundle 字节数据的开销较大,建议所有开发者在使用 WWW.LoadFromCacheOrDownload 方法时,确保 AssetBundles 都比较小——最多几兆字节。同样建议所有开发者在内存比较有限的平台(比如移动设备)上使用这个方法时,确保同时只下载一个 AssetBundle,防止内存泄漏。

如果缓存文件夹没有足够的空间来缓存额外的文件,LoadFromCacheOrDownload 将会从缓存中迭代删除最近最少使用的 AssetBundles,直到有足够的空间来存储新的 AssetBundle。如果空间还是不够(比如硬盘满了,或者所有缓存的文件都正在被使用),LoadFromCacheOrDownload() 将绕开缓存,直接将文件以流的形式存进内存。

如果想要使用 LoadFromCacheOrDownload 的版本变量,方法参数(第二个参数)需要改变。如果参数与当前缓存的 AssetBundle 的版本变量一致,那么就可以从缓存中加载这个 AssetBundle。

UnityWebRequest

UnityWebRequest 有个专门的 API 来处理 AssetBundles。首先,你需要使用 UnityWebRequest.GetAssetBundle 方法来创建你的 web 请求。在请求返回后,将请求放入 DownloadHandlerAssetBundle.GetContent(UnityWebRequest) 作为参数。GetContent 方法将返回你的 AssetBundle 对象。

在下载完 AssetBundle 后,你同样可以使用 DownloadHandlerAssetBundle 类的 assetBundle 属性来加载 AssetBundle,这就和使用 AssetBundle.LoadFromFile 方法一样高效。

下面有个例子展示:如何加载一个包含两个 GameObjects 的 AssetBundle,并实例化它们。想要运行这段程序,我们只需要调用 StartCoroutine(InstantiateObject()) 方法:

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

使用 UnityWebRequest 的优点是,它允许开发者用更灵活的方式来处理下载的数据,并且潜在地排除了不必要的内存占用。和 UnityEngine.WWW 类相比,这是更现代,也更推荐的 API。

从 AssetBundles 中加载资源

现在,你已经成功下载了你的 AssetBundle,是时候从中加载一些资源。

通常的代码片段:

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

T 是你想加载的资源类型。

当你决定如何加载资源的时候,有一对方法供使用。我们可以使用 LoadAsset、LoadAllAssets 方法,以及与它们对应的异步方法: LoadAssetAsync、LoadAllAssetsAsync。

下面是一个从一个 AssetBundle 中同步加载资源的例子:

加载一个 GameObject:

GameObject gameObject = loadedAssetBundle.LoadAsset<GameObject>(assetName);

加载所有资源:

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

现在,和上面展示的方法(要么返回你正在加载的对象,要么返回一组对象)不同的是,异步方法返回的是一个 AssetBundleRequest。

在可以使用资源前,你需要等待处理完成。如下:

AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;
AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();
yield return request;
var loadedAssets = request.allAssets;
一旦你已经加载好你的资源,是时候行动了!你可以像使用 Unity 中的其他对象一样使用加载的对象。

加载 AssetBundle Manifests(资源清单)

加载 AssetBundle manifests 非常的有用。尤其是当处理 AssetBundle 依赖关系的时候。为了获取可以使用的 AssetBundleManifest,你需要加载一个额外的 AssetBundle(即那个和文件夹名称相同的文件),并且从中加载出一个 AssetBundleManifest 类型的对象。从 AssetBundle 中加载 manifest 完全和从中加载其他资源一样,如下:

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

现在,你可以通过上面例子获取到的 manifest 对象来使用 AssetBundleManifest 类的 API。从现在开始,你可以使用这个 manifest 来获取关于 AssetBundle 的信息,包括:依赖数据、hash 数据,以及版本变量数据。

还记得前面章节我们讨论过的,如果一个 bundleA 对 bundleB 有依赖,那么在从 bundleA 中加载任何资源之前,我们需要先加载 bundleB 吗?Manifest 对象就使得动态查找正在加载的依赖关系成为可能。比如我们想要加载一个名叫“assetBundle”的 AssetBundle 的所有依赖:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies(assetBundle");
foreach(string dependency in dependencies)
    AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));

现在,你已经加载了 AssetBundle、AssetBundle 依赖,以及其他资源,是时候讨论如何管理这些加载好的 AssetBundles 了。

在 Objects 被从场景中移除的时候,Unity 不会自动将它们卸载。资源的清理是在某个特定时机被触发,当然也可以手动触发。

知道什么时候加载和卸载一个 AssetBundle 很重要。不合时宜的卸载 AssetBundle 可能导致重复对象(duplicating objects)错误,或者其他未预料到的情况,比如纹理丢失

理解如何管理 AssetBundle 最重要的事是什么时候调用 AssetBundle.Unload(bool) 方法,以及该方法的参数应该传入 true 还是 false。该方法卸载 AssetBundle 的头信息;方法参数决定了是否同时卸载从 AssetBundle 中加载并实例化的所有 Objects。

如果你传入 true 参数,那么你从 AssetBundle 中加载的所有对象将被卸载,即便这些对象正在被使用。这就是我们前面提到的,导致纹理丢失的原因。

假设 Material M 是从 AssetBundle AB 中加载的,如下:

如果 AB.Unload(true) 被调用,那么任何使用 Material M 的实例都将被卸载并消除,即便它们正在场景中被使用。

如果 AB.Unload(false) 被调用,那么将切断所有使用 Material M 的实例与 AssetBundle AB 的联系。

如果 AssetBundle AB 在被卸载后不久再次被加载,Unity 并不会将已经存在的使用 Material M 的实例与 AssetBundle AB 重新联系。因此将存在两份被加载的 Material M。

通常情况下,使用 AssetBundle.Unload(false) 不会获得理想情况。大多数项目应该使用 AssetBundle.Unload(true) 方法,以避免内存中出现重复对象(duplicating objects)。

大多数项目应该使用 AssetBundle.Unload(true) 方法,并且要采取措施确保没有重复对象。两种通常采取的措施如下:

在应用的生命周期中找到合适的时机来卸载 AssetBundle,比如关卡之间,或者加载场景的时候。

为每个对象采取引用计数管理方法,只有当 AssetBundle 的所有对象都没有被使用的时候,再卸载 AssetBundle。这样就可以避免应用出现重复对象的问题。

如果应用必须使用 AssetBundle.Unload(false) 方法,对象将只能在以下两种情况下被卸载:

消除对象的所有引用,包括场景中的和代码中的。之后,调用

Resources.UnloadUnusedAssets。

没有额外附加特性地加载一个场景。这将消除当前场景的所有对象,并自动调用 Resources.UnloadUnusedAssets。

如果你不想自己管理加载的 AssetBundle、依赖关系,以及资源,你可能需要使用 AssetBundle 管理器。(AssetBundle Manager,下一章节将介绍。)

public class LoadAssetBundle : MonoBehaviour {
	public string manifestFilePath;
	// Use this for initialization
	void Start () {
		manifestFilePath = Application.streamingAssetsPath + "/StreamingAssets";
	void GetData(){
		AssetBundle assetBundle = AssetBundle.CreateFromFile(manifestFilePath);
		AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
		string[] dependencies = manifest.GetAllDependencies(mater.assetbundle");
		foreach(string dependency in dependencies)
			AssetBundle.CreateFromFile(Application.streamingAssetsPath+"/"+dependency);