Unity 补完计划(二):AssetBundle
AB 包的概念
Unity 资源管理
在Unity中,一般来说,资源加载方式主要分为Resources加载和AssetBundle加载。
Unity有个特殊文件夹Resources,放在这个文件夹下的资源可以通过Resources.Load()
来直接加载。即Resources加载资源方式。
当获得AssetBundle之后,也可以调用AssetBundle对应的API来加载资源。
什么是 AB 包
AB包全名AssetBundle(资源包)。是一种Unity提供的用于存放资源的包。通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,并且可以有选择地加载内容。
为什么要用AB包
-
热更新。(要热更新需要确保AB包打出来的资源具有唯一性,且相同资源的AB包检验码相同。)
资源热更新主要通过AssetBundle来实现,在Unity编辑器内为游戏中所用到的资源指定AB包的名称和后缀,然后进行打包并上传服务器,待游戏运行时动态加载服务器上的AB资源包。
-
Resources加载虽然简单方便,但是也有很多问题:
- 对内存管理造成一定的负担。
- 在打开应用时加载时间很长。
- Resources 文件夹下的所有资源统一合并到一个序列化文件中(可以看成统一打一个大包,巨型AB包有什么问题它就有什么问题),对资源优化有一定的限制。
AB 包使用方法
打AB包
1 | public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform); |
BuildAssetBundleOptions
是一个枚举类型,它的值转化为二进制都只有一位是 1,其他位都是 0。如
UncompressedAssetBundle
是 0000 0000 0001
, IgnoreTypeTreeChanges
是 0000 0100 0000
,DisableLoadAssetByFileName
是 1000 0000 0000
。
BuildAssetBundles底层会对传入的BuildAssetBundleOptions值进行处理,根据二进制位数来判断使用哪种策略构建AB包。因此如果在构建AB包时想要使用多种策略,用&连接即可。
BuildTarget参数用来选择针对的平台,因为AB包在不同平台下是不兼容的。
获取AB包方法
1 | AssetBundle.LoadFromFile(string path) |
LoadFromFile
是从文件中加载AB包,它从一个给定的路径来加载AB包。如果AB包是LZ4加载方式,它只会加载AB包的Header,之后需要什么资源再加载那部分的AB包chunk。极大的减少了内存占用。(LoadFromFileAsync
是它的异步版本)。
LoadFromMemory
是从内存中加载AB包,它从内存中的byte[]
中加载AB包。它会完整的把AB包加载出来。(LoadFromMemoryAsync
是它的异步版本)
LoadFromStream
是从流中加载AB包,它从一个Stream中加载AB包。跟LoadFromFile
一样,如果AB包是LZ4加载方式,它也是只会加载AB包的Header。(LoadFromStreamAsync
是它的异步版本)
WWW
是Unity中的跟网络相关的类,可以通过该类从网络中下载资源,之后加载成AB包。
还有很多关于AssetBundle的方法,官方API中有详细的介绍。
AB包内部结构
-
AssetBundleFileHeader
:记录了版本号、压缩等主要描述信息。 -
AssetFileHeader
:包含一个文件列表,记录了每个资源的name、offset、length等信息。 -
Asset1:
AssetHeader
:记录了TypeTree大小、文件大小、format等信息。TypeTree
(可选,有不要TypeTree的构建方式):记录了Asset对象的class ID。Unity可以用class ID来序列化和反序列化一个类。(每个class对应了一个ID,如0是Object类,1是GameObject类等。具体可在Unity官网上查询。)ObjectPath
:记录了path ID(资源唯一索引ID)等。AssetRef
:记录了AB包对外部资源对引用情况。
-
Asset2: …
AB包变体
即 AssetBundleVariant。AB包变体被用来支持定制化参数,允许不同AB包中的不同Object在加载和解析instance ID 引用时显示为相同 Object。
从概念上讲,允许两个 Object 显示为共享相同的 GUID 和 Local ID,但实际上由 Variant ID 来区分。
简而言之,实际上就是一个资源的分类标签。
如同一图片的高清和低清资源,同一模型的高精度和低精度资源。
在Unity编辑器右下角设置AB包名的后面就是设置AB包变体名。
LZMA和LZ4
LZMA是流压缩方式(stream-based)。流压缩再处理整个数据块时使用同一个字典,它提供了最大可能的压缩率,但是只支持顺序读取。所以加载AB包时,需要将整个包解压,会造成卡顿和额外内存占用。
LZ4是块压缩方式(chunk-based)。块压缩的数据被分为大小相同的块,并被分别压缩。如果需要实时解压随机读取,块压缩是比较好的选择。LoadFromFile()
和LoadFromStream()
都只会加载AB包的Header,相对LoadFromMemory()
来说大大节省了内存。
AB 包内存占用
下面是AB包在内存中的占用情况:
这是从网络中下载资源的内存占用情况。
下载的资源包括AB包、图片、材质、动画、音频等,以Stream的形式存储在内存中。(AB包中也可以有图片、材质、动画、音频等资源)
之后通过加载AB包的方法,将AB包加载到内存中去。
AB包内的资源需要通过AssetBundle.Load()
来加载到内存中。
对于GameObject来说,通常情况下需要对其进行改动,所以它是完全复制一份该资源来进行的实例化。也就是说,当AB包中的GameObject从内存中卸载后,实例化的GameObject不会因此丢失。并且对实例化对象的修改不会影响到GameObject资源。
对于Shader和Texture来说,通常情况下不需要对其进行改动,所以它是通过引用来进行的实例化。也就是说,当AB包中的Shader和Texture资源从内存中卸载后,实例化的Shader和Texture会出现资源丢失的情况。并且对实例化对象的修改会影响到Shader和Texture资源。
对于Material和Mesh来说,有时候可能需要对其进行改动,所以它是通过引用+复制来进行的实例化。也就是说,当AB包中的Material和Mesh资源从内存中卸载后,实例化的Material和Mesh会出现资源丢失的情况。并且对实例化对象的修改不会影响到Material和Mesh资源。
总结大致流程为:AB包先要从硬盘或者网络中加载到内存中,然后将AB包内的每一份资源加载到内存中,再之后在内存中实例化这些资源。每种资源有其自己不同的实例化方式,卸载资源的时候需要注意。
使用 AB 包的注意事项
AB包的依赖问题
依赖问题,通俗的话来说就是A包中某资源用了B包中的某资源。然而如果A包加载了,B包没有加载,这就会导致A包中的资源出现丢资源的现象。
在Unity5.0后,BuildAssetBundleOptions.CollectDependencies
永久开启,即Unity会自动检测物体引用的资源并且一并打包,防止资源丢失遗漏的问题出现。
因为这个特性,有些情况下,如果没指定某公共资源的存放在哪个AB包中,这个公共资源就会被自动打进引用它的AB包中,所以出现多个不同的AB包中有重复的资源存在的现象,这就是资源冗余。
这种情况下,哪怕资源是一模一样,也无法进行合并优化。
要防止资源冗余,就需要明确指出资源存放在哪个AB包中,形成依赖关系。所以对于一些公共资源,建议单独存放在一个AB包中。
在加载的时候,如果AB包之间相互依赖,那么加载一个AB包中的资源时,先需要加载出另一个AB包的资源。这样就会导致不必要的消耗。所以说尽可能地减少AB包之间的依赖,并且公共资源尽量提前加载完成。
细粒度问题
细粒度问题即每个AB包分别放入多少资源的问题,一个好的策略至关重要。
加载资源时,先要加载AB包,再加载资源。如果AB包使用了LZMA或LZ4压缩算法,还需要先给AB包解压。
特点 | AB包数量较多,包内资源较少 | AB包数量较少,包内资源较多 |
---|---|---|
加载时间 | 加载一个AB包到内存的时间短,玩家不会有卡顿感,但每个资源实际上加载时间变长。 | 加载一个AB包到内存的时间较长,玩家会有卡顿感,但之后包内的每个资源加载很快。 |
热更新 | 灵活,要更新下载的包体较小。 | 不灵活,要更新下载的包体较大。 |
IO次数 | 过多,增大了硬件设备耗能和发热压力。 | 不多,硬件压力小。 |
简单策略:
- 经常更新和不经常更新的对象拆分到不同的AB包中。
- 同时加载的对象放在一个AB包中。
- 不可能同时加载的对象拆分到不同的AB包中。
- 根据项目逻辑功能来分组打AB包。
- 根据同一类型对象来分组打AB包。
- 公共资源和非公共资源拆分到不同的AB包中。
AB包的加密
因为AB包存放着游戏的各种资源,所以如果AB包不加密,那么别人在得到AB包的时候可以直接看到AB包内所有的资源。经过一定特殊操作后可以直接从AB包中导出图片、音频、动画,甚至可以在Unity中直接实例化出来另存为Prefab。
加密思路如下:
-
在构建完AB包后,可以将AB包中的内容以byte[]形式读取。
-
之后选用任意加密方式对该byte[]加密。
-
加密完后重新写入AB包中。
-
AB包加密完成。
这样对AB包加密之后,如果使用AssetBundle.LoadFromFile()
来加载加密的AB包是会报错的,因为Unity以及无法识别加密过后的内容了,这样也就防止了别人随意对AB包进行的读取和加载,保证了资源的安全性。
解密思路如下:
-
先以byte[]形式读取AB包中的内容。
-
之后使用对应的解密算法对该byte[]进行解密。
-
解密过后的byte[]通过
AssetBundle.LoadFromMemory()
来进行加载。 -
AB包加载完成。
总的来说,这种二进制加密AB包的方式虽然有效,但是加载时间和内存占用是一个需要考虑的问题。很多时候选择不进行加密,一方面原因是因为需要多占用一份内存的问题,代价过大。虽然说从byte[]
加载成AB包之后,byte[]
可以从内存中释放,但是在加载的过程中还是会有一个内存占用的巅峰。
另一种简单的加密方式,即可以实现直接手段加载不出AB包,而且相对上述二进制加密AB包方式加载更快、耗费更小。
本质是通过在AB包中添加偏移量来实现加密。
1 | public static AssetBundle LoadFromStream(Stream stream, uint crc, uint managedReadBufferSize) |
AssetBundle.LoadFromFile()
的第三个参数是AB包内容的byte偏移量。也就是说从offset个byte开始读取AB包的内容。
因此如果在构建完AB包之后,在AB包前插入N个随机byte,那么此时想要加载该AB包,如不知道这个N值,则是无法成功读取和加载AB包的。这也就实现了加密。