AssetBundle详解
AssetBundle可以理解为一种,它可以将特定平台的(模型、纹理、预制体、音频等)包含在内,并在运行时由Unity加载。通过AssetBundle技术,可以在游戏中实现资源的热更新。通过热更新,一方面可以避免每次更新资源都需要用户重新下载整个游戏,另一方面也可以减小游戏安装包的大小,将资源文件延迟到更新时下载。另外,开发者还可以根据实际情况,选择内置的压缩算法来压缩AssetBundle,从而
1.什么是AssetBundle
AssetBundle可以理解为一种包文件,它可以将特定平台的非代码资源(模型、纹理、预制体、音频等)包含在内,并在运行时由Unity加载。通过AssetBundle技术,可以在游戏中实现资源的热更新。通过热更新,一方面可以避免每次更新资源都需要用户重新下载整个游戏,另一方面也可以减小游戏安装包的大小,将资源文件延迟到更新时下载。另外,开发者还可以根据实际情况,选择内置的压缩算法来压缩AssetBundle,从而提高网络传输效率。
AssetBundle的组成
首先要明确一点,AssetBundle是一种容器,一个容器中可以包含其他文件。这些包含的文件一般分为两种:
- 一个序列化文件。如模型、预制体等被拆解为一个个单独的对象,然后统一写入到一个文件中。
- 资源文件。如图片、音频等二进制资源被单独存放,以便快速加载。
2.如何使用AssetBundle
构建AssetBundle
首先我们在场景中创建一个Cube,并将其添加为预制体。此时在预制体的Inspector面板中就可以看到AssetBundle的选项
点击这个下拉菜单就可以为其新建一个AssetBundle名称。注意这个名称是支持目录形式的,比如prefab/cube
,它表示的是在prefab文件夹下创建名为cube的AssetBundle。如果两个不同的资源选择了同一个AssetBundle名称,则这两个资源会被一同打包到该AssetBundle包中。
另外,左侧的下拉菜单可以为AssetBundle自定义一个文件后缀,上图的AssetBundle构建出来后会以.ab
结尾。(这里吐槽一句,一旦名称命名错了,只能将当前AssetBundle名称置空,然后选择Remove Unused Names
才能删除,十分反人类!)
为AssetBundle分配好了资源,就可以进行构建了。不过Unity似乎并未内置Build的快捷选项,因此我们需要自己动手。代码也很简单,就是使用BuildPipeline
提供的BuildAssetBundles
API。代码如下:
[MenuItem("Framework/AssetBundleHelper/Build", false, 1)]
private static void BuildAssetBundle()
{
string path = "Assets/AssetBundles";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
BuildPipeline.BuildAssetBundles(path, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
AssetDatabase.Refresh();
}
AssetDatabase.Refresh
的作用是刷新工程目录的缓存,让新增的文件立刻显示在project面板中。
经过一次构建,我们就会发现在设定的目录下生成了几个文件:
很显然,这些文件是成对出现的,一种是不带任何后缀(包括自己定义后缀)的文件,另一种是.manifest
文件。其中cube.ab
文件是我们构建出来的AssetBundle本体,cube.ab.manifest
是这个AssetBundle对应的清单文件。除此之外,还生成了一个与根目录同名的AssetBundle文件及其.manifest
文件。这些文件具体的作用,在后面会讲到。
加载并使用AssetBundle
在AssetBundle构建完成后,我们就可以在运行时通过代码获取到对应的资源。这一步的核心API是AssetBundle.LoadFromFile
和AssetBundle.LoadAsset
。其用法如下:
private static void LoadAssetBundle()
{
string path = "Assets/AssetBundles";
AssetBundle assetBundle = AssetBundle.LoadFromFile(path + "/prefab/cube.ab");
if (assetBundle is null)
{
Debug.Log("指定AssetBundle不存在");
return;
}
GameObject cube = assetBundle.LoadAsset<GameObject>("Cube");
Instantiate(cube);
}
上面用到的这两个API也有对应的异步加载方式,后面会详细说明。
3.AssetBundle分组策略
还记得在打包之前,不同的资源可以选择相同的AssetBundle名称吗?这其实就是AssetBundle的分组。我们可以按照某种策略,将符合条件的一组资源分配进同一个AssetBundle。下面列举几个常见的分组策略:
- 逻辑实体分组
- 一个UI界面或所有UI界面一个包(贴图、布局信息)
- 一个角色或所有角色一个包(模型、动画)
- 场景之间共享的资源一个包
- 类型分组
- 所有音频资源一个包
- 所有模型资源一个包
- 所有材质资源一个包
- 并发内容分组
- 同一时间内需要用到的资源一个包
- 一个关卡所需的资源一个包
- 一个场景所需的资源一个包
分组策略需要根据具体的项目来定制,但分组的核心逻辑都是尽可能的减少资源更新时所需的代价。举个栗子,假如将需要频繁更新的资源A与不需要频繁更新的资源B打包在一起,那么每次A资源更新,就需要下载AB组成的整个包,而B又不需要更新,也就是说每次下载都会有一部分数据是毫无必要的。Unity的官方文档中给出了一些建议可供参考:
- 将频繁更新的资源与较少更新的资源拆分到不同的AssetBundle
- 将可能同时加载的对象分到一组。例如模型及其纹理和动画
- 将其他包共享的资源放到一个单独的包中。
- 如果不可能同时加载两组对象(例如标清资源和高清资源),请确保它们位于各自的 AssetBundle 中。
- 如果一个 AssetBundle 中只有不到 50% 的资源经常同时加载,请考虑拆分该AssetBundle
- 考虑将多个小型的(少于 5 到 10 个资源)但经常同时加载内容的 AssetBundle 组合在一起
- 如果一组对象只是同一对象的不同版本,可以考虑通过后缀加以区分
依赖打包
上面提到了将其他包共享的资源放到一个单独的包中,我们可以通过一个具体的实例来验证为什么要这样做。
首先创建两个游戏物体,让他们依赖同一个材质,然后制成预制体
将这两个预制体打包进不同的AssetBundle
打包后的结果如下,可以看到两个包加起来大概有1M的大小
那么如果将这两个预制体依赖的材质资源单独打一个包,结果又会如何呢?
打包的结果如下
可以看到,三者的总体积要比之前小了很多。这是因为Unity在默认情况下,不会对依赖资源打包策略进行优化。Cube依赖了Material,那么cube包中也会包含Material;如果另外有一个Sphere也依赖了Material,那么sphere包中同样会包含Material。如果将Material单独包含进一个AssetBundle,在打包时,Unity就会识别出包之间的依赖,从而避免将依赖项一同打入包中。
但这样做也会存在风险。将材质提取到单个AssetBundle后,如果只使用预制体实例化游戏物体,就会出现材质丢失的情况
解决方案是在使用依赖资源之前,先加载依项的AssetBundle。(其实在cube实例化后再加载依赖包,其材质也会成功加载,只不过在依赖包加载完成之前,cube还是材质丢失的状态)
private static void LoadDependencyAssetBundle()
{
string path = "Assets/AssetBundles";
AssetBundle cubeAb = AssetBundle.LoadFromFile(path + "/cube");
AssetBundle materialAb = AssetBundle.LoadFromFile(path + "/material");
if (cubeAb is null)
{
Debug.Log("指定AssetBundle不存在");
return;
}
GameObject cube = cubeAb.LoadAsset<GameObject>("Cube");
Instantiate(cube);
}
加载正常
4.AssetBundle压缩
在Build AssetBundle的代码中,有一个BuildAssetBundleOptions的枚举类型参数,它主要控制的是Build时的一些可选项,且支持逻辑运算。
BuildPipeline.BuildAssetBundles(path, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
这个枚举类型也列举出了Build时要采用的压缩算法:
BuildAssetBundleOptions.None
这是默认采用的压缩策略。使用LZMA压缩算法,压缩的包会比较小,但是加载时间会比较长,在使用前需要进行整体解压。一旦被解压,这些包就会重新采用LZ4算法重新进行压缩。LZ4是基于块的压缩算法,当Unity需要从LZ4压缩的包中访问资源时,只需要解压缩并读取包含所请求资源的字节块即可。优点是加载时间短,但相应的包的体积也会比较大。BuildAssetBundleOptions.UncompressedAssetBundle
不进行压缩。包占用空间很大,但加载速度会很快。BuildAssetBundleOptions.ChunkBasedCompression
使用LZ4进行压缩,包比较大,但加载速度也会比较快。
5.Manifest文件
.manifest
文件就是在打包时生成的清单文件,可以用文本编辑器打开。打开后会有如下所示的内容
ManifestFileVersion: 0
CRC: 1610761843
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: 521947ff3d4497aceb05856a22ce7e4c
TypeTreeHash:
serializedVersion: 2
Hash: 4db82f4e618e4630b2fa8228e9c6d8d1
HashAppended: 0
ClassTypes:
- Class: 1
Script: {instanceID: 0}
- Class: 4
Script: {instanceID: 0}
- Class: 21
Script: {instanceID: 0}
- Class: 23
Script: {instanceID: 0}
- Class: 33
Script: {instanceID: 0}
- Class: 43
Script: {instanceID: 0}
- Class: 65
Script: {instanceID: 0}
SerializeReferenceClassIdentifiers: []
Assets:
- Assets/01_AssetBundlePractice/AssetBundle/Cube.prefab
Dependencies:
- [工程路径]/Assets/AssetBundles/material
其中比较重要的是最下面的两个路径。Assets记录了这个AssetBundle包含了哪个资源,Dependencies记录了这个AssetBundle依赖于哪些其他的AssetBundle。除了每个AssetBundle对应的.manifest
文件,在根目录中还有一个以跟文件夹命名的.manifest
文件,它打开后会有如下所示内容
ManifestFileVersion: 0
CRC: 3248957812
AssetBundleManifest:
AssetBundleInfos:
Info_0:
Name: cube
Dependencies:
Dependency_0: material
Info_1:
Name: sphere
Dependencies:
Dependency_0: material
Info_2:
Name: material
Dependencies: {}
可以看到,它主要记录了所有AssetBundle包的名称及其依赖信息。这个清单文件可以帮助我们获取到任意一个AssetBundle的依赖项。要获取到Manifest只需要像一般的AssetBundle一样进行加载,然后载入其中的AssetBundleManifest即可(其实这种方式加载的是AssetBundle包中的manifest文件)
string path = "Assets/AssetBundles";
AssetBundle assetBundle = AssetBundle.LoadFromFile(path + "/AssetBundles");
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 获取指定AssetBundle的所有依赖
string[] cubeAllDependencies = manifest.GetAllDependencies("cube");
// 获取指定AssetBundle的直接依赖
string[] sphereDirectDependencies = manifest.GetDirectDependencies("sphere");
// 获取所有AssetBundle
string[] assetBundles = manifest.GetAllAssetBundles();
6.加载AssetBundle
加载AssetBundle有四种方式,分别是从本地文件加载、从流加载、从内存加载以及从远程服务器加载。它们分别对应如下几个API:
AssetBundle.LoadFromFile
从本地加载AssetBundle.LoadFromMemory
从内存中加载AssetBundle.LoadFromStream
从流中加载UnityWebRequestAssetBundle.GetAssetBundle
从远程服务器加载(也可以从本地加载)
AssetBundle.LoadFromFile
这是加载AssetBundle最快的方法。如果AssetBundle未压缩或采用了数据块(LZ4)压缩方式,LoadFromFile 将直接从磁盘加载AssetBundle。使用此方法加载完全压缩 (LZMA) 的AssetBundle将首先解压缩AssetBundle,然后再将其加载到内存中。它的使用方法如下:
// 同步方式
private static void LoadMethodSync()
{
string path = "Assets/AssetBundles/cube";
AssetBundle assetBundle = AssetBundle.LoadFromFile(path);
GameObject cube = assetBundle.LoadAsset<GameObject>("Cube");
Instantiate(cube);
}
// 异步方式
IEnumerator LoadMethodAsync()
{
string path = "Assets/AssetBundles/cube";
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);
yield return request;
var cube = request.assetBundle.LoadAsset<GameObject>("Cube");
Instantiate(cube);
}
AssetBundle.LoadFromMemory
该方法传入一个包含AssetBundle数据的字节数组。也可以根据需要传递CRC校验码。如果捆绑包采用的是LZMA压缩方式,将在加载时解压缩AssetBundle。LZ4 压缩包则会以压缩状态加载。当下载的是加密数据并需要从未加密的字节创建AssetBundle时会用到。它的使用方法如下:
// 同步方法
private static void LoadMethodSync()
{
string path = "Assets/AssetBundles/cube";
AssetBundle assetBundle = AssetBundle.LoadFromMemory(File.ReadAllBytes(path));
Instantiate(cube);
}
// 异步方法
IEnumerator LoadMethodAsync()
{
string path = "Assets/AssetBundles/cube";
AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
yield return request;
var cube = request.assetBundle.LoadAsset<GameObject>("Cube");
Instantiate(cube);
}
AssetBundle.LoadFromStream
从托管Stream加载AssetBundle。如果是LZMA压缩,则将数据解压缩到内存。如果是未压缩或使用块压缩的捆绑包,则直接从Stream读取。需要注意的是,在加载AssetBundle或其中的资源时,不应该释放Stream资源,应该在AssetBundle.Unload之后再释放。相比于从内存加载,从流加载的优势是加载加密资源时,占用的内存较小。它的用法如下:
// 同步方法
private static void LoadMethodSync()
{
AssetBundle.UnloadAllAssetBundles(true);
string path = "Assets/AssetBundles/cube";
FileStream stream = new FileStream(path, FileMode.Open,FileAccess.Read);
AssetBundle assetBundle = AssetBundle.LoadFromStream(stream);
GameObject cube = assetBundle.LoadAsset<GameObject>("Cube");
Instantiate(cube);
assetBundle.Unload(false);
stream.Close();
}
// 异步方法
IEnumerator LoadMethodAsync()
{
string path = "Assets/AssetBundles/cube";
FileStream stream = new FileStream(path, FileMode.Open,FileAccess.Read);
AssetBundleCreateRequest request = AssetBundle.LoadFromStreamAsync(stream);
yield return request;
var cube = request.assetBundle.LoadAsset<GameObject>("Cube");
Instantiate(cube);
request.assetBundle.Unload(false);
stream.Close();
}
UnityWebRequestAssetBundle.GetAssetBundle
该方法会创建经过优化的 UnityWebRequest,以通过 HTTP GET 下载AssetBundle。请求成功后,使用DownloadHandlerAssetBundle
方法将数据流式传输到缓冲区并解压缩。与一次性下载所有数据相比,这种方式节省了很多内存。需要注意,如果使用UnityWebRequestAssetBundle.GetAssetBundle
加载本地数据,需要在路径中加上file://
前缀。其用法如下:
IEnumerator LoadMethodAsync()
{
string path = @"[远程资源路径]";
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(path);
yield return request.SendWebRequest();
AssetBundle assetBundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = assetBundle.LoadAsset<GameObject>("Cube");
Instantiate(cube);
}
7.从AssetBundle加载资源
AssetBundle包加载完成后,就可以加载包中的资源。前面已经多次用到,这里只罗列出对应的API
string path = "Assets/AssetBundles/cube";
AssetBundle assetBundle = AssetBundle.LoadFromFile(path);
// 同步加载指定资源
GameObject cube = assetBundle.LoadAsset<GameObject>("Cube");
// 异步加载指定资源
AssetBundleRequest request1 = assetBundle.LoadAssetAsync<GameObject>("Cube");
// 同步加载所有资源
UnityEngine.Object[] objs = assetBundle.LoadAllAssets();
// 异步加载所有资源
AssetBundleRequest request2 = assetBundle.LoadAllAssetsAsync();
8.AssetBundle卸载
在AssetBundle的资源使用完成后,需要对其进行卸载,以释放其占用的内存空间。AssetBundle的卸载主要靠AssetBundle.Unload
这个API实现。该方法需要传入一个bool类型的参数,如果传入的是true,则会卸载AssetBundle本身及从AssetBundle加载的全部资源。如果传入的是false,则会保留已经加载的资源。
在大多数情况下都推荐使用AssetBundle.Unload(true)
,因为如果传入false会造成如下问题(图片来自Unity官方文档):
当参数为false时,会中断材质资源M与当前AssetBundle的联系
即便再次加载 AB 并且调用 AB.LoadAsset(),Unity也不会将现有 M 副本重新链接到新加载的材质
如果现在创建了一个预制体的实例并引用了材质M,也不会使用现有的M,而是会加载一个新的M副本
这样就导致了M在内存中存在两份,造成内存资源的浪费。因此在大多数情况下应该使用AssetBundle.Unload(true)
来保证对象不会重复。两种常用的方式是:
- 在应用程序生命周期中具有明确定义的卸载AssetBundle的时间点,例如在关卡之间或在加载期间。
- 维护单个对象的引用计数,仅当未使用所有组成对象时才卸载 AssetBundle。这允许应用程序卸载和重新加载单个对象,而无需复制内存。
如果不得不使用AssetBundle.Unload(false)
,则只能用以下两种方式卸载单个对象:
- 在场景和代码中消除对不需要的对象的所有引用。完成此操作后,调用
Resources.UnloadUnusedAssets
。 - 以非附加方式加载场景。这样会销毁当前场景中的所有对象并自动调用
Resources.UnloadUnusedAssets
。
8.AssetBundle浏览工具
Unity官方提供了AssetBundle的浏览工具插件,但并没有默认集成,需要我们手动进行安装。安装方式如下:
- 在项目中打开 Unity Package Manager(菜单:Windows > Package Manager)。
- 单击窗口左上角的 +(添加)按钮。
- 选择 Add package from git URL…
- 输入
https://github.com/Unity-Technologies/AssetBundles-Browser.git
作为 URL - 单击 Add。
安装完成后,通过Window > AssetBundle Browser打开工具窗口。通过该工具可以查看项目中所有AssetBundle信息,也可以进行Build操作。操作比较简单,这里不再赘述。
参考文献:
[1]Siki学院.AssetBundle(创建打包)入门学习(基于Unity2017)[DB/OL].https://www.sikiedu.com/my/course/74.
[2]Unity官方手册[DB/OL].https://docs.unity3d.com/cn/current/Manual/AssetBundlesIntro.html.
更多推荐
所有评论(0)