「翻译」Unity中的AssetBundle详解(四)

AssetBundle依赖关系

如果一个或多个UnityEngine.Objects包含位于另一个bundle中的UnityEngine.Object的引用,则AssetBundles可以依赖于其他AssetBundles。如果UnityEngine.Object包含一个在其他任何AssetBundle中都不包含的UnityEngine.Object的引用,则不会发生依赖关系。在这种情况下,在构建AssetBundles时,将bundle所依赖的对象的副本复制到捆绑包中。如果多个bundle中的多个对象包含对未分配给bundle的同一对象的引用,那么对该对象具有依赖关系的每个bundle将各自制作一个该对象的副本并将其打包到内置的AssetBundle中。
如果AssetBundle包含依赖关系,则在加载要尝试实例化的对象之前,加载包含这些依赖关系的bundles是重要的。 Unity不会尝试自动加载依赖关系。
考虑以下示例,Bundle 1中的材料引用了Bundle 2中的纹理:
在此示例中,加载Bundle 1中的Material之前,需要将Bundle 2加载到内存中。Bundle 1Bundle 2的加载顺序无关紧要,重要的是在从Bundle 1加载Material之前加载Bundle 2。在下一节中,我们将讨论如何使用在上一篇博客中所涉及的AssetBundleManifest对象,在运行时确定和加载依赖关系。

使用本地AssetBundles

在 Unity 5 中,我们可以使用四种不同的 API 来加载 。它们的行为根据正在加载的平台和 AssetBundles 构建时使用的压缩方式(未压缩,LZMA,LZ4)而有所不同。
我们需要用到的四个 API 是:

  • AssetBundle.LoadFromMemoryAsync
  • AssetBundle.LoadFromFile
  • WWW.LoadfromCacheOrDownload
  • UnityWebRequest’s DownloadHandlerAssetBundle (Unity 5.3 或者更高的版本)

AssetBundle.LoadFromMemoryAsync

AssetBundle.LoadFromMemoryAsync
此函数使用包含 AssetBundle 数据的字节数组的参数。如果需要也可以传递一个 CRC 值。如果 Bundle 是LZMA 压缩的,它将在加载时解压缩 AssetBundle。 LZ4 压缩的 Bundle 在压缩状态时被加载。
以下是使用此方法的一个示例:

1
2
3
4
5
6
7
8
9
10
11
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);
}

但是,这不是唯一可以使用 LoadFromMemoryAsync 的策略。 可以用任何获取所需的字节数组的过程替代File.ReadAllBytes(path) 方法。

AssetBundle.LoadFromFile

AssetBundle.LoadFromFile
从本地存储加载未压缩的 Bundles 时,此API非常高效。如果 Bundle 是未压缩或块(LZ4)压缩的,LoadFromFile 将直接从磁盘加载 Bundle。使用此方法加载完全压缩(LZMA)的 Bundle 将首先解压缩包,然后再将其加载到内存中。
如何使用 LoadFromFile 的一个例子:

1
2
3
4
5
6
7
8
9
10
11
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 或更早版本的Android设备上,尝试从 Streaming Assets 路径加载 AssetBundles 时,此API将失效。这是因为该路径的内容将驻留在压缩的 .jar 文件中。 Unity 5.4和更新版本可以使用这个API 加载 Streaming Assets 路径下的资源。

WWW.LoadFromCacheOrDownload

WWW.LoadFromCacheOrDownload

**此 API 将被废弃 (请使用 UnityWebRequest) **

此API可用于从远程服务器下载 AssetBundles 或加载本地 AssetBundles。它是 UnityWebRequest API 的较旧且不太理想的版本。
从远程位置加载 AssetBundle 将自动缓存 AssetBundle。如果AssetBundle被压缩,那么一个工作线程将自动解压缩该包并将其写入缓存。一旦捆绑包解压缩并缓存,它将像 AssetBundle.LoadFromFile 一样加载。
如何使用 LoadFromCacheOrDownload 的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 对象中的字节的内存开销,建议使用 WWW.LoadFromCacheOrDownload
的所有开发人员确保其 AssetBundles 保持较小——最多为几兆字节。还建议在限制内存平台(如移动设备)上运行的开发人员确保其代码一次只下载一个 AssetBundle,以避免内存尖峰。
如果缓存文件夹没有用于缓存附加文件的空间,LoadFromCacheOrDownload将从缓存中迭代删除最近最少使用的AssetBundle,直到有足够的空间可用于存储新的AssetBundle。如果无法进行空间(因为硬盘已满,或者当前正在使用缓存中的所有文件)释放,LoadFromCacheOrDownload() 将绕过缓存并将文件以流的形式加入内存。
为了强制 LoadFromCacheOrDownload,版本参数(第二个参数)将需要更改。如果传递给该函数的版本与当前缓存的AssetBundle的版本匹配,则AssetBundle将仅从缓存加载。

UnityWebRequest

UnityWebRequest

UnityWebRequest 有一个特定的 API 调用来处理 AssetBundles。首先,你需要使用
UnityWebRequest.GetAssetBundle 创建您的 Web 请求。返回请求后,将请求对象传递给DownloadHandlerAssetBundle.GetContent(UnityWebRequest)。此 GetContent 函数调用将返回AssetBundle 对象。
你还可以在下载 Bundle 后使用 DownloadHandlerAssetBundle 类中的 assetBundle 属性,以AssetBundle.LoadFromFile 的效率加载 AssetBundle。
以下是一个如何加载包含两个 GameObject 的 AssetBundle 并实例化它们的示例。要开始这个过程,我们只需要调用 StartCoroutine(InstantiateObject());

1
2
3
4
5
6
7
8
9
10
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,是时候最终加载某些资产了。
通用代码段:

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

T 是尝试加载的资产的类型。
决定如何加载资产时,有几种选择。分别是 LoadAsset 、LoadAllAssets 和它们的对应的异步方法 LoadAssetAsync 和 LoadAllAssetsAsync。
下面代码展示了如何从AssetBundles同步加载资产:
加载单个 GameObject:

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

加载所有资产:

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

现在,正如上文所示的方法返回的对象类型或要加载的对象的数组,异步方法返回一个 AssetBundleRequest。访问资产之前,需要等待此操作完成。加载一个 Asset:

1
2
3
AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;

以及

1
2
3
AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();
yield return request;
var loadedAssets = request.allAssets;

一旦你加载了你所需要的 Assets,接下来就可以像使用 Unity 中任何其他对象一样使用加载的对象。

加载 AssetBundle 清单文件(Manifests)

加载 AssetBundle 清单文件非常有用。特别是在处理 AssetBundle 依赖项时。
要获得一个可用的 AssetBundleManifest 对象,你需要加载额外的 AssetBundle(名称与其所在文件夹相同),并从中加载类型为 AssetBundleManifest 的对象。
加载清单本身与加载 AssetBundle 中的任何其他资产完全相同:

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

现在,你可以通过上述示例中的 manifest 对象访问 AssetBundleManifest API 的调用。从这里你可以使用清单来获取有关你所构建的 AssetBundles 的信息。该信息包括 AssetBundles 的依赖关系数据、散列数据和变体数据。

在前面的部分,当我们讨论 AssetBundle Dependencies 并且如果一个 bundle 对另一个 bundle 有依赖关系,那么在从原始 bundle 加载任何资源之前,需要加载这些 bundle。清单对象使得动态地找到加载依赖性成为可能。假设我们要加载名为 “assetBundle” 的 AssetBundle 的所有依赖项。

1
2
3
4
5
6
7
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));
}

上面代码表示正在加载 AssetBundles、AssetBundle依赖关系和资产,现在是时候讨论管理所有这些加载的 AssetBundles 了。

管理加载的 AssetBundles

另请参阅:Unity 官方教程中有关 Managing Loaded AssetBundles的教程。

当它们从活动场景中删除时,Unity不会自动卸载对象。资产清理在特定时间触发,也可以手动触发。知道何时加载和卸载 AssetBundle 很重要。不正确卸载 AssetBundle 可能会导致内存中的对象复制或其他不合要求的情况(如缺少纹理)。
了解 AssetBundle 管理最重要的是什么时候调用 AssetBundle.Unload(bool),并且在函数调用时你应该将 true 或 false 作为参数传递给函数。卸载 AssetBundle 是一个非静态的功能,此 API 卸载正在调用的 AssetBundle 的头部信息。该函数的参数表示是否也卸载从此 AssetBundle 实例化的所有对象。

如果使用 AssetBundle.Unload(true) 方法,就会卸载从 AssetBundle 中加载的所有对象,即使它们正在当前活动的场景中使用。这正是我们前面提到的,这可能会导致纹理丢失。我们假设材质 M 是从 AssetBundle AB 加载的,如下所示。如果调用 AB.Unload(true)。活动场景中的任何 M 实例也将被卸载和销毁。如果你改为调用 AB.Unload(false),它会破坏当前 M 和 AB 实例的链接。

如果以后再次加载 AB,并调用 AB.LoadAsset(),Unity 将不会将 M 的现有副本重新链接到新加载的材质。那么 M 会加载两个副本。

一般来说,使用 AssetBundle.Unload(false) 并不会导致理想的情况。大多数项目应该使用 AssetBundle.Unload(true) 来避免在内存中复制对象。
大多数项目应该使用 AssetBundle.Unload(true) 并采用一种方法来确保对象不被重复。两种常见的方法是:

  • 在应用程序的生命周期中具有明确定义的地方,卸载临时 AssetBundles,例如关卡之间或加载屏幕期间。
  • 维护单个对象的引用计数,并仅在其所有组成对象未使用时卸载 AssetBundles。这允许应用程序卸载和重新加载单个对象而没有重复的内存。

如果应用程序必须使用 AssetBundle.Unload(false),那么单个对象只能以两种方式卸载:

  • 在场景和代码中消除对不需要的对象的所有引用。完成之后,调用 Resources.UnloadUnusedAssets
  • 使用非叠加式加载场景。这将销毁当前场景中的所有对象并自动调用 Resources.UnloadUnusedAssets

如果你不想自己管理加载 AssetBundles、依赖关系和 Assets 本身,你可能会发现自己需要 AssetBundle Manager。

原文链接:

  1. AssetBundle Dependencies
  2. Using AssetBundles Natively

同系列文章

「翻译」Unity中的AssetBundle详解(一)

「翻译」Unity中的AssetBundle详解(二)

「翻译」Unity中的AssetBundle详解(三)

「翻译」Unity中的AssetBundle详解(四)

「翻译」Unity中的AssetBundle详解(五)