Unity——浅谈AB包(AssetBundle)

浅谈AB包


Unity资源管理

在Unity中,一般来说,资源加载方式主要分为Resources加载和AssetBundle加载。

Unity有个特殊文件夹Resources,放在这个文件夹下的资源可以通过Resources.Load()来直接加载。即Resources加载资源方式。

当获得AssetBundle之后,也可以调用AssetBundle对应的API来加载资源。


什么是AB包

AB包全名AssetBundle(资源包)。是一种Unity提供的用于存放资源的包。通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,并且可以有选择地加载内容。


为什么要用AB包

1、热更新。(要热更新需要确保AB包打出来的资源具有唯一性,且相同资源的AB包检验码相同。)

2、Resources加载虽然简单方便,但是也有很多问题:

  • 对内存管理造成一定的负担。
  • 在打开应用时加载时间很长。
  • Resources文件夹下的所有资源统一合并到一个序列化文件中(可以看成统一打一个大包,巨型AB包有什么问题它就有什么问题),对资源优化有一定的限制。
  • 不建议大量使用Resources。

使用方法

打AB包:

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包名:

AssetImporter.assetBundleName // AB包名
AssetImporter.assetBundleVariant // AB包变体名

获取AB包方法:

AssetBundle.LoadFromFile(string path)
AssetBundle.LoadFromFileAsync(string path)
AssetBundle.LoadFromMemory(byte[] binary)
AssetBundle.LoadFromMemoryAsync(byte[] binary)
AssetBundle.LoadFromStream(Stream stream)
AssetBundle.LoadFromStreamAsync(Stream stream)
WWW.assetBundle

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.LoadAsset(string assetName, Type resType)
AssetBundle.LoadAssetAsync(string assetName, Type resType)

LoadAsset是同步方法,LoadAssetAsync是异步方法。


还有很多关于AssetBundle的方法,官方API中有详细的介绍。


AB包变体

即AssetBundleVariant。AB包变体被用来支持定制化参数,允许不同AB包中的不同Object在加载和解析instance ID引用时显示为相同Object。

从概念上讲,允许两个Object显示为共享相同的GUID和Local ID,但实际上由Variant ID来区分。

简而言之,实际上就是一个资源的分类标签。

如同一图片的高清和低清资源,同一模型的高精度和低精度资源。

在Unity编辑器右下角设置AB包名的后面就是设置AB包变体名。


BuildAssetBundleOptions:

None - 0:默认方式。使用LZMA压缩算法,该算法压缩后包体很小,但是加载的时候需要花费很长的时间解压。第一次解压之后,该包又会使用LZ4压缩算法再次压缩。这就是为什么第一次加载时间长,之后加载时间就没那么长了。(LZMA需要完整解压之后才能加载包内资源,LZ4不需要完整解压就可以加载包内资源。)

UncompressedAssetBundle - 1:不压缩。虽然包体大,但是加载快。

DisableWriteTypeTree - 8:不包含TypeTree信息。虽然可以使得AB包更小,但是对低版本不兼容。不建议使用。

DeterministicAssetBundle - 16:创建一个哈希来映射存储在AB包里的对象的id。

ForceRebuildAssetBundle - 32:强制重建AB包。

IgnoreTypeTreeChanges - 64:当做增量构建检测时,忽略type tree的变化。

AppendHashToAssetBundleName - 128:添加哈希到AB包名。

ChunkBasedCompression - 256:使用基于块的LZ4压缩算法。

StrictMode - 512:如果在构建时有任何错误,则不允许构建成功。

DryRunBuild - 1024:干构建。

DisableLoadAssetByFileName - 4069:禁止AB包通过文件名加载资源。

DisableLoadAssetByFileNameWithExtension - 8192:禁止AB包通过文件扩展名加载资源。

AssetBundleStripUnityVersion:构建时从压缩文件和序列化文件的header中移除Unity版本号。


LZMA和LZ4

LZMA是流压缩方式(stream-based)。流压缩再处理整个数据块时使用同一个字典,它提供了最大可能的压缩率,但是只支持顺序读取。所以加载AB包时,需要将整个包解压,会造成卡顿和额外内存占用。

LZ4是块压缩方式(chunk-based)。块压缩的数据被分为大小相同的块,并被分别压缩。如果需要实时解压随机读取,块压缩是比较好的选择。LoadFromFile()和LoadFromStream()都只会加载AB包的Header,相对LoadFromMemory()来说大大节省了内存。


内存占用

下面是AB包再内存中的占用情况:

AssetBundle内存

这是从网络中下载资源的内存占用情况。

下载的资源包括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包内部结构

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…


.manifest

这是AB包对应的.manifest文件。

ManifestFileVersion: 0 # 文件版本
CRC: 2657307167 # CRC校验码
Hashes: # 哈希
  AssetFileHash: # AB包中所有资源的哈希,可用于增量更新检测
    serializedVersion: 2 # Unity序列化版本
    Hash: 717e408ba50ee41b0960161fd2d5a827
  TypeTreeHash: # AB包中所有类型的哈希,可用于增量更新检测
    serializedVersion: 2 # Unity序列化版本
    Hash: 8d552bf2f5bdba1177c938cb98ca6f2f
HashAppended: 0
ClassTypes: # TypeTree
- Class: 1 # GameObject
  Script: {
    
    instanceID: 0}
- Class: 21 # Material
  Script: {
    
    instanceID: 0}
- Class: 28 # Texture2D
  Script: {
    
    instanceID: 0}
- Class: 48 # Shader
  Script: {
    
    instanceID: 0}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: -1200242548, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: -146154839, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: 1297475563, guid: f70555f144d8491a825f0804e09c671c, type: 3}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: 11500000, guid: 20e8969313b8e4614b498f042e99683a, type: 3}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: 11500000, guid: c86dbe77db44a434bb15895563508b65, type: 3}
- Class: 114 # MonoBehaviour
  Script: {
    
    fileID: 11500000, guid: 1a7e2f4cb82d9b94a91270d550c880c0, type: 3}
- Class: 115 # MonoScript
  Script: {
    
    instanceID: 0}
- Class: 128 # Font
  Script: {
    
    instanceID: 0}
- Class: 198 # ParticleSystem
  Script: {
    
    instanceID: 0}
- Class: 199 # ParticleSystemRenderer
  Script: {
    
    instanceID: 0}
- Class: 213 # Sprite
  Script: {
    
    instanceID: 0}
- Class: 222 # CanvasRenderer
  Script: {
    
    instanceID: 0}
- Class: 224 # RectTransform
  Script: {
    
    instanceID: 0}
- Class: 687078895 # SpriteAtlas
  Script: {
    
    instanceID: 0}
Assets: # 包含资源
- Assets/Bundle/.../a.prefab
- Assets/Bundle/.../b.prefab
- Assets/Bundle/.../c.spriteatlas
Dependencies: # AB包依赖
- /Users/apple/.../AssetBundles/Android/q
- /Users/apple/.../AssetBundles/Android/w
- /Users/apple/.../AssetBundles/Android/e
- /Users/apple/.../AssetBundles/Android/r
- /Users/apple/.../AssetBundles/Android/t

特殊路径

Resources

对应的是Resources特殊文件夹路径。(只读)

在Unity下对应为:/Assets/Resources。


Application.streamingAssetsPath

对应的是StreamingAsset文件夹路径。(只读)

在Unity下对应为:/Assets/StreamingAssets。

在Android下对应为:jar:file:///data/app/xxx.apk!/assets。

在iOS下对应为:Application/…/xxx.app/Data/Raw。


Application.persistentDataPath

对应的是应用持久化数据存储文件夹路径。应用更新、覆盖安装时,这里的数据都不会被清除。(可读可写)

在Unity下对应为:/该Unity项目文件夹路径。

在Android下对应为:/…/data/应用名/files。

在iOS下对应为:Application/…/Documents。iOS还会自动将persistentDataPath路径下的文件上传到iCloud,会占用用户的iCloud空间。如果persistentDataPath路径下的文件过多,苹果审核可能被拒,所以,iOS平台,有些数据得放temporaryCachePath路径下。


Application.dataPath

对应的是应用Asset文件夹路径。(只读。Android不可读,因为改目录指向的是个.apk文件,而不是目录)

在Unity下对应为:/Assets。

在Android下对应为:/data/app/…/xxx.apk。

在iOS下对应为:Application/…/xxx.app/Data。


Application.temporaryCachePath

对应的是应用临时数据缓存文件夹路径。(只读)

在Unity下对应为:/该Unity项目文件夹路径。

在Android下对应为:/…/data/应用名/cache。

在iOS下对应为:Application/…/Library/Caches。


依赖问题

依赖问题,通俗的话来说就是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次数过多,增大了硬件设备耗能和发热压力。 IO次数不多,硬件压力小。

简单策略:

  • 经常更新和不经常更新的对象拆分到不同的AB包中。
  • 同时加载的对象放在一个AB包中。
  • 不可能同时加载的对象拆分到不同的AB包中。
  • 根据项目逻辑功能来分组打AB包。
  • 根据同一类型对象来分组打AB包。
  • 公共资源和非公共资源拆分到不同的AB包中。

卸载问题

当调用Resources.UnloadAsset()时,虽Object被销毁,但Instance ID被保留且包含有效的GUID和Local ID引用。

当调用AssetBundle.Unload(true)时,不仅Object被销毁,而且Instance ID的GUID和Local ID引用变无效。

当调用AssetBundle.Unload(false)时,虽Object不被销毁,但Instance ID的GUID和Local ID引用变无效。场景中的物体会与该AB包分离链接。即该物体的instance ID引用的GUID和Local ID会断开引用,无法再通过该instance ID找到GUID和Local ID。

如果再次加载该AB包时,分离了链接的物体不会受该新加载的AB包管理。因此如果不注意的话可能会导致一些不可控的问题。Unity中有Resources.UnloadUnusedAssets()方法可以很好地解决这个问题。


AB包的加密

因为AB包存放着游戏的各种资源,所以如果AB包不加密,那么别人在得到AB包的时候可以直接看到AB包内所有的资源。经过一定特殊操作后可以直接从AB包中导出图片、音频、动画,甚至可以在Unity中直接实例化出来另存为Prefab。

加密思路如下:

1、在构建完AB包后,可以将AB包中的内容以byte[]形式读取。

2、之后选用任意加密方式对该byte[]加密。

3、加密完后重新写入AB包中。

4、AB包加密完成。

这样对AB包加密之后,如果使用AssetBundle.LoadFromFile()来加载加密的AB包是会报错的,因为Unity以及无法识别加密过后的内容了,这样也就防止了别人随意对AB包进行的读取和加载,保证了资源的安全性。

解密思路如下:

1、先以byte[]形式读取AB包中的内容。

2、之后使用对应的解密算法对该byte[]进行解密。

3、解密过后的byte[]通过AssetBundle.LoadFromMemory()来进行加载。

4、AB包加载完成。

总的来说,这种二进制加密AB包的方式虽然有效,但是加载时间和内存占用是一个需要考虑的问题。很多时候选择不进行加密,一方面原因是因为需要多占用一份内存的问题,代价过大。虽然说从byte[]加载成AB包之后,byte[]可以从内存中释放,但是在加载的过程中还是会有一个内存占用的巅峰。

另一种简单的加密方式,即可以实现直接手段加载不出AB包,而且相对上述二进制加密AB包方式加载更快、耗费更小。

本质是通过在AB包中添加偏移量来实现加密。

public static AssetBundle LoadFromFile(string path, uint crc, ulong offset);

AssetBundle.LoadFromFile()的第三个参数是AB包内容的byte偏移量。也就是说从offset个byte开始读取AB包的内容。

因此如果在构建完AB包之后,在AB包前插入N个随机byte,那么此时想要加载该AB包,如不知道这个N值,则是无法成功读取和加载AB包的。这也就实现了加密。


从Stream中加载

AssetBundle.LoadFromStream()不像AssetBundle.LoadFromMemory()会多占用一份内存。

public static AssetBundle LoadFromStream(Stream stream, uint crc, uint managedReadBufferSize)

这是从托管流中加载AB包的方法。它跟LoadFromFile()一样,只会读取AB包的头文件。

使用Stream加载的限制:

1、AB包数据必须是从Stream的0位置开始。

2、当从AssetBundle数据的末尾开始并尝试读取数据时,Stream实现必须返回读取的0字节且不引发异常。

3、Stream必须是可读(CanRead返回true)和可搜寻(CanSeek返回true)的。

4、可以从任何Unity线程中调用Seek()和Read()。


CRC校验

AB包加载资源的完整方法实际上是AssetBundle.LoadFromFile(string path, uint crc, ulong offset),三个参数。其中第二个参数就是CRC校验符。

每个AB包的.manifest文件中也有CRC校验符,用于校验数据完整性。


各种ID

序列化后,资源用GUID和Local ID管理。

GUID对应Asset。GUID存在.meta文件中。提供了文件特定位置的抽象。是一种映射。无需关心资源在磁盘上的存放位置。

Local ID对应Asset内的每一个Object。(Asset中)


虽然GUID和Local ID比较好用,但是毕竟因为存在磁盘上,读取比较耗时。因此Unity缓存一个instance ID对应Object,通过instance ID快速找到Object。instance ID是一种快速获取对象实例的ID,包含着对GUID和Local ID的引用。解析instance ID可以快速返回instance表示的已加载对象,如果为加载目标对象,则可以将文件GUID和Local ID解析为对象源数据,从而允许Unity即时加载对象。每次AB包重新加载时,都会为每个对象创建新的instance ID。


总结

没有最好的打AB包方式,只有最适合项目的打AB包方式。


参考资料

https://docs.unity3d.com/ScriptReference/AssetBundle.html
https://docs.unity3d.com/Manual/ClassIDReference.html
https://www.xuanyusong.com/?s=AssetBundle
https://blog.csdn.net/lodypig/category_6315960.html
https://blog.csdn.net/BillCYJ/article/details/99712313
https://learn.unity.com/tutorial/assets-resources-and-assetbundles#5c7f8528edbc2a002053b5a6



后记

这是很早之前学习AB包的时候写的笔记,有很多地方理解不到位,欢迎各位进行指正和讨论。

猜你喜欢

转载自blog.csdn.net/weixin_42186870/article/details/106968839