在Unity中,一般来说,资源加载方式主要分为Resources加载和AssetBundle加载。
Unity有个特殊文件夹Resources,放在这个文件夹下的资源可以通过Resources.Load()来直接加载。即Resources加载资源方式。
当获得AssetBundle之后,也可以调用AssetBundle对应的API来加载资源。
AB包全名AssetBundle(资源包)。是一种Unity提供的用于存放资源的包。通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,并且可以有选择地加载内容。
1、热更新。(要热更新需要确保AB包打出来的资源具有唯一性,且相同资源的AB包检验码相同。)
2、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中有详细的介绍。
即AssetBundleVariant。AB包变体被用来支持定制化参数,允许不同AB包中的不同Object在加载和解析instance ID引用时显示为相同Object。
从概念上讲,允许两个Object显示为共享相同的GUID和Local ID,但实际上由Variant ID来区分。
简而言之,实际上就是一个资源的分类标签。
如同一图片的高清和低清资源,同一模型的高精度和低精度资源。
在Unity编辑器右下角设置AB包名的后面就是设置AB包变体名。
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是流压缩方式(stream-based)。流压缩再处理整个数据块时使用同一个字典,它提供了最大可能的压缩率,但是只支持顺序读取。所以加载AB包时,需要将整个包解压,会造成卡顿和额外内存占用。
LZ4是块压缩方式(chunk-based)。块压缩的数据被分为大小相同的块,并被分别压缩。如果需要实时解压随机读取,块压缩是比较好的选择。LoadFromFile()和LoadFromStream()都只会加载AB包的Header,相对LoadFromMemory()来说大大节省了内存。
下面是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包内的每一份资源加载到内存中,再之后在内存中实例化这些资源。每种资源有其自己不同的实例化方式,卸载资源的时候需要注意。
AssetBundleFileHeader:记录了版本号、压缩等主要描述信息。
AssetFileHeader:包含一个文件列表,记录了每个资源的name、offset、length等信息。
Asset1:
Asset2…
这是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次数不多,硬件压力小。 |
简单策略:
当调用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包中导出图片、音频、动画,甚至可以在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包的。这也就实现了加密。
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()。
AB包加载资源的完整方法实际上是AssetBundle.LoadFromFile(string path, uint crc, ulong offset),三个参数。其中第二个参数就是CRC校验符。
每个AB包的.manifest文件中也有CRC校验符,用于校验数据完整性。
序列化后,资源用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包的时候写的笔记,有很多地方理解不到位,欢迎各位进行指正和讨论。