分类目录归档:AssetBundle 完全手册
第三章 AssetBundle内存分析之——内存去哪了?
首先要介绍几个Profiler中的名词:
- ManagedHeap.UsedSize (Mono环境下堆使用内存量)
- ManagedHeap.ReservedUnusedSize (Mono环境下堆空闲内存量)
内存堆只会增加不会减小,当内存被释放后,内存并不会还给系统,内存首先会使用堆上空闲部分,当空闲部分不足时才会向系统申请新的空间,在移动平台上一个 app 如果使用的内存过大导致系统无法承受就会被系统强制结束,造成闪退的出现。
本章节将引用一个Unity工程进行说明,相关工程可自行下载:https://github.com/mister91jiao/AssetBundleMemory
第一步 加载AssetBundle
首先我们准备一个 AssetBundle,这里就直接使用演示工程的资源。
将三张贴图分别做成三个材质,并且创建三个 Cube 将材质赋予,然后将这些资源构建成一个 AssetBundle,命名为 testab.bundle,可以看到这个 AssetBundle 的大小是 1603kb 也就是 1.56mb。
将工程以调试模式打包,运行起来后,我们通过 Profiler 查看,发现其内存消耗如图所示。这时候开始进行第一步,将 AssetBundle 加载到内存。
然后再次对 profiler 进行观察。
通过对比我们发现,其中 Other 项里多了 2.9mb 的内存,但我们的资源明明 只有 1.56 mb,这是为什么呢?这个时候将 Other 项展开得到图
我们发现在 AssetBundle 下面 有一个 LoadingCache 的项占用了整整 1mb 的内存,但这个并不是我们的资源,这个是因为 Unity 需要有一个索引表去缓存每个 AssetBundle 内都有哪些资源,这样当再次加载其中的某一个资源的时候,可以加速查找,而这个缓存表的默认大小就是 1mb,会在第一次加载 AssetBundle 的时候生成,关于这部分可以参考Unity的官方手册:https://docs.unity3d.com/2021.2/Documentation/ScriptReference/AssetBundle-memoryBudgetKB.html。
去掉这 1mb 的内存后,在 ManagedHeap.UsedSize 里就有我们加载的数据以及 AssetBundle 对象所占用的堆内存的大小,大约是 2mb 这与我们的资源量大小对应。
在 BundleMaster 中如果不使用异或加密并且压缩格式不是 LZMA ,则加载 AssetBundle 使用的 API 是 LoadFromFile,就不需要将数据缓存到堆上。但本次演示依然使用 LoadFromMemory 的方式进行 AssetBundle 的加载。
第二步 加载资源
我们将 AssetBundle 里所有的 Prefab 加载出来,因为他们每个 Prefab 都依赖了贴图,可以说相当于把整个 AssetBundle 里的内容加载出来,然后对 profiler 进行观察。
然后发现,Assets 部分有了明显增加,而其它部分并未改变,我们将 Assets 项展开。
当把 AssetBundle 里的资源,主要是那几张图片加载出来后,明显占用了内存,尤其是那个 zfnp,一个资源就占用了 9.6mb 的内存。
第三步 实例化
刚才提到,我们加载的是 Prefab,现在将 GameObject 全部示例化,并将实例化后的对象存在 _gameObjects 数组里。
然后再观察 profiler。
可以看到 Scene Memory 项有一点增加,这是因为当我们实例化对象的时候,对象身上的一些组件都会被创建出来,如果展开的化就会发现增加的这些内存是因为对象身上的组件造成的,可以说对象实例化的开销对内存很小。
第四步 销毁实例化的对象
将刚才实例化的对象进行销毁,并且重新查看 profiler。
可以看到内存占用回到了实例化之前的状态。
第五步 卸载AssetBundle
将 AssetBundle 卸载后,再查看profiler。
可以看到 Assets 项已经被卸载,回到了初始的样子,但 Other 项 似乎并没有发生什么变化,这是为什么呢?展开看看。
原来 AssetBundle 的 LoadingCache 并没有回收,因为只要加载过 AssetBundle 就会一直有这个,其中 ManagedHeap.UsedSize 确实减少了,但减少的部分跑到了 ManagedHeap.ReservedUnusedSize 上,当下次再加载的时候就不需要再向系统申请内存了,直接用 ManagedHeap.ReservedUnusedSize 里的内存空间,再重新加载一下 AssetBundle 验证一下。
可以看到 218.6mb 的占用和第一次加载 AssetBundle 的时候一样,而 ManagedHeap.ReservedUnusedSize 减少了,ManagedHeap.UsedSize 的占用增加了。
最后补充
从上面的测试流程来看,已经清楚的说明了 AssetBundle 内存占用的来龙去买,但这都是在 Mono 环境下运行的,在 IL2CPP 的情况下会不会有所不同呢?虽然 Unity 的 IL2CPP 是将代码转为 C++ 运行,但依然是托管内存,不过不可以直接在 Profiler 里看到 ManagedHeap.UsedSize 和 ManagedHeap.ReservedUnusedSize 这两个参数,如果想看到占用堆内存的情况,可以通过 pinvoke 的方式调用 il2cpp 的代码来获取数据。
91焦先生
2022/07/20
第二章 了解AssetBundle的生命周期
当我们构建好一个AssetBundle之后,当然是要加载它,Unity 提供了三种加载 AssetBundle的API,分别是 :
- AssetBundle.LoadFromMemory 通过 byte[] 数组加载,无论用什么方法只要拿到资源的 byte[] 数据就可以加载,比较灵活,BundleMaster 在使用异或加密的时候用这种方式。
- AssetBundle.LoadFromFile 通过路径加载,好处是可以直接加载 StreamingAssets 下的资源,并且不会像 LoadFromMemory 那样占用额外的堆内存(LZMA压缩的情况下不生效),BundleMaster 默认使用此模式。
- AssetBundle.LoadFromStream 通过文件流加载,限制较多,但可以自己设定 Unity 加载数据时使用的读取缓冲区大小从而提升加载性能,能优化一点点性能,不过一般不用。
当加载完成后,我们就获得了一个 AssetBundle 对象,我们可以从这个对象里获取我们想要的资源,如果这个资源是 GameObject,则可以将加载出来的资源实例化,就得到了游戏中具体的物体,如果需要多个物体也可以实例化多次。
如果想要卸载某个资源,需要先将所有实例化出来的物体清除,然后对 AssetBundle 对象调用 Unload,但是 Unload 有一个参数 bool unloadAllLoadedObjects:
- 当 unloadAllLoadedObjects 为 true 的时候,卸载会将这个 AssetBundle 加载出的所有资源都给卸载掉,BundleMaster 使用的就是这个。
- 当 unloadAllLoadedObjects 为 false的时候,卸载 AssetBundle 只会卸载原始数据和加载出来没有引用的资源,而拥有引用的资源并不会卸载,如果想要卸载掉这类资需要将引用取消并且手动调用 Resources.UnloadUnusedAssets。
如果使用 Unload(false) 的方式,可以在资源被从 AssetBundle 中加载出来的时候就将 AssetBundle 卸载,可以节省一点点内存的使用,但是如果一个 AssetBundle 当中拥有很多资源,再加载这个 AssetBundle 中其它资源的时候就又需要将 AssetBundle 重新加载出来,浪费掉的性能与省下的那一点点内存相比得不偿失,更何况 AssetBundle 都是经过高度压缩的,本来占用内存就不大,所以综合来看还是使用 Unload(true) 的机制比较好。
可能有朋友对上面的过程中诸如 AssetBundle 和 AssetBundle加载出的资源有所疑问,欢迎看下一章,会着重分析这里。
91焦先生
2022/07/19
第一章 了解Unity目录与加载方式
1. Resources,StreamingAssets 和 PersistentDataPath
在序言中已经提到说 Resources 可以用来存放资源并提出一些缺陷,但Resources依然还是需要用到的,主要是用来存放一些不需要热更的特殊文件,通常是一些配置文件,或者可能是游戏的logo以及启动画面之类的,这些资源很小,而且也不需要更新,所以可以放到 Resources 文件夹下。在打包的时候,Unity会把工程下所有 Resources 文件夹进行合并压缩在一起。
StreamingAssets 和 PersistentDataPath,这两个文件夹都是存放的原生资源,也就是文件夹下有什么,打包完成后里面就是什么,并不会对文件进行更改,但在PC平台与移动平台上,StreamingAssets 是有所区别的。在PC平台上如 Windows,StreamingAssets 可读可写,而在移动平台上如 Android,StreamingAssets 就只有可读权限了,并且不可以直接用C#的IO类进行加载,如 FileStream 类,也没有办法使用C#的 File.Exists 判断资源是否存在,想要读取 StreamingAssets 下的资源,可以用 UnityWebRequest 的方式去异步加载资源,如果资源是 AssetBundle,也可以用 AssetBundle.LoadFromFile 来加载。BundleMaster 将 LocalBundlePath 默认设置为 StreamingAssets,用来存放首包资源,而之后更新的内容,会将更新部分存放在 PersistentDataPath 目录下。(Tips. 什么是首包资源? 一些项目资源内容并不大,希望将资源放在包体内也就是 StreamingAssets 下,这份资源就是首包资源。)
PersistentDataPath 目录在移动平台下没有限制,既可读也可写,并且可以正常使用C#的IO类。BundleMaster 将更新目录也就是 HotfixPath 默认设置为 PersistentDataPath ,但要注意的是 PersistentDataPath 目录在 Windows 下 在C盘,因此使用BundleMaster的时候推荐在开发阶段编辑器下的时候,将更新目录也就是 HotfixPath 重新映射为其它地方。
打包的时候非 Resources 和 StreamingAssets 下的资源都会被丢弃掉,我们正好可以利用这一特性,创建一个其它文件夹用于存放我们的资源,而最后会将资源构建成 AssetBundle,我们只需要 AssetBundle 在包体里即可,资源正好可以在打包的时候丢弃掉,不会在包里占用额外的空间。
2. 加载资源的方式
对于加载使用到的API这里不再赘述,可以查阅Unity的官方文档。Resources 只可以 加载Resources 文件夹下的资源,可以不填资源后缀。BundleMaster 使用的是 LoadFromMemory 通过 byte[] 加载 AssetBundle ,比较灵活。AssetDatabase.LoadAssetAtPath 是只有在编辑器下才可以使用的加载API,它可以加载所有路径下的资源,并且不管Unity是否是运行时都可以加载,但它只能同步加载没有办法进行异步。
在 BundleMaster 中提供了三种运行模式。分别是:
- Develop
- Local
- Build
其中 Develop 模式下所有加载API都是调用的 AssetDatabase.LoadAssetAtPath ,这样在开发阶段中不需要构建 AssetBundle 包,非常方便,但与之相对的所有异步加载接口在 Develop 下都是同步执行的。Local 模式则是跳过检查更新的流程,直接使用 LocalBundlePath 下的资源。Build 模式则是正常检查更新,并且将更新的部分放在 HotfixPath下,会优先加载HotfixPath下的资源。
91焦先生
2022/07/18
序 为什么需要AssetBundle?
说到 unity 的ab,通常会想到热更新,在当前游戏开发以手游为主的大环境下,热更新又是许多手游必不可少的需求。手游的热更新需要分两个部分,一是代码的热更新,二是资源的热更新,而要想实现资源的热更新,则就必须使用Unity的 AssetBundle 系统。但如果没有热更新需求,那是不是就不用使用 AssetBundle 了呢?答案当然是否定的。
首先说一下传统的 Resources.Load 加载资源的缺陷,要想使用 Resources.Load 进行加载资源,需要把资源放在 Resources 文件夹下,但这个文件夹在打包的时候会被放入包体,而在移动平台下,对单个应用包体的大小是有限制的,超过了就出不了包。Android 是限制2G,而 iOS 是限制4G,尽管 Android 可以通过OBB 分包的形式将资源分离出来突破2G的限制,但绝大多数内容分发平台都不支持,而且安装一个 .apk 还要额外再安装 .obb 也太麻烦了。但 数据持久化路径 也就是 Application.persistentDataPath 或者又叫做 沙盒路径,对存储的大小是没有限制的,这个路径是在 app 安装后产生,因此我们可以将资源构建成 AssetBundle,在app安装后下载到这个路径下使用,这样就突破了应用大小的限制并且用户也只用安装一次。
那么如果资源大小没有超过2G也不需要热更新,或者干脆开发PC端游戏,是不是就可以用 Resources.Load 加载资源了呢?如果硬用的话,可以是可以但用户体验非常糟糕。Unity开发的游戏在启动的时候,会初始化Resources资源的索引表,Resources 下的资源越多,这个过程就越慢,慢到难以忍受!因此为了游戏的启动速度,也是需要使用 AssetBundle 的。
综上所述,如果想要好好开发一款游戏,AssetBundle 必不可少。
91焦先生
2022/07/18