资源加载的多种方式
虽然资源的格式并不一定要依照引擎来,但如果自己另开辟一种格式来做为自定义资源格式确实耗时耗力,性价比很难适合,虽然也要按项目的需求来,如果是那种资源保密性要求很强的,其实也可以借助Unity3D自身的机制来完成加密工作(下面的章节中会介绍加密)。
这里我们还是主要来说说以Unity3D自身格式为重点的资源加载方式。
我们可以把资源加载分为阻塞式和非阻塞式。到底什么是阻塞式什么是非阻塞式呢?
简单来说,阻塞是当前资源加载完了才能执行下一条语句,非阻塞是开另一个线程(或协程)加载资源,当前的主线程可以继续执行程序,当加载完毕时再通知主线程。
在Unity3D中阻塞式的加载主要有:
1.Resource.Load
Resource.Load是传统的资源加载方式,Unity3D通过Resources这个名字的文件夹来加载资源。
在移动设备下,Unity3D打包了Resources文件夹的所有资源文件成为1个或几个资源文件(是资源文件合并成1个或几个资源包文件)放入包内,当调用Resource.Load时从这几个资源文件中加载。
这个资源包文件会被Unity3D在打包时压缩,保证了包体的大小会适度的减少,压缩的另一面是解压,因此在通过Resource.Load加载时也增加了解压的CPU损耗。这也是很多项目不乐意使用Resources的缘由,解压消耗带给他们不必要的开销,因为CPU资源比硬盘资源珍贵的多。
2.File read + AssetBundle.CreateFromMemory + AssetBundle.Load
我们也可以先通过文件操作加载资源文件,再通过AssetBundle.CreateFromMemory的方式把byte数据转换成AssetBundle格式,再通过AssetBundle.Load从AssetBundle中加载某个资源。
这种方式看起来费时费力,但是这种方式可以加入我们些许自定义功能。比如能在加载AssetBundle前做加解密操作,加载AssetBundle前自主加载了文件,文件的数据的加解密方式就可以自由的把控,我们可以先用文件操作获得数据后解密,再转换成AssetBundle实例,最后交给资源控制程序处理。
不过获得加解密AssetBundle的能力,是需要付出代价的,代价就是内存和GC(内存的分配与销毁)。
由于用文件操作时完全读入了整个文件的数据,导致当前还不需要的资源也一并读入内存,增大了内存消耗,另外转换成AssetBundle后的byte数据也不再由用处,等待GC的消耗动作,大大增加了内存分配和销毁的CPU负荷。
3.AssetBundle.CreateFromFile + AssetBundle.Load
我们还可以使用通过直接加载文件变成AssetBundle的方式,再通过AssetBundle.Load接口来获得资源。
这种加载方式最大的好处是按需分配内存。AssetBundle.CreateFromFile并不会把所有资源文件整个加载进内存中,而是先加载数据头,通过数据头中的数据去识别各个资源在文件中的偏移位置,当调用AssetBundle.Load时,根据数据头中对应资源偏移量的记录,找到资源位置,加载数据进入内存,因此我们说它是按需分配内存的。
在Unity3D中非阻塞式的加载有:
1.AssetBundle.CreateFromFile + AssetBundle.LoadAsync 2.WWW + AssetBundle.Load 3.WWW + AssetBundle.LoadAsync 4.File Read all + AssetBundle.CreateFromMemory + AssetBundle.Load 5.File Read all + AssetBundle.CreateFromMemory + AssetBundle.LoadAsync 6.File Read async + AssetBundle.CreateFromMemory + AssetBundle.Load 7.File Read async + AssetBundle.CreateFromMemory + AssetBundle.Load 8.File Read async + AssetBundle.CreateFromMemory + AssetBundle.LoadAsync
几种方式主要是由文件读取和AssetBundle异步加载形式组合而成。
前2种为主流的异步加载方式。其中第1种用的比较多,因为大多数资源文件都会在游戏开始前进行比对和下载,所以没必要使用WWW的形式从本地读取或从网络下载。
其实这里涉及到“为什么要用非阻塞加载”的问题。阻塞式加载这么好用,为什么还要用非阻塞式。
我们不要为了异步而异步,有人会觉得异步更高级,如果只是为了异步而做异步是没有意义的。大部分情况下我们在使用阻塞式加载资源时,都会遇到一个问题,在某一帧加载的资源很多,加载完毕后需要实例化的资源也很多,从而导致画面在这一帧耗时特别长,画面卡顿现象特别不严重,用运营同学们的话说“对用户来说不友好”。为了能更好更平滑的过度场景,我们需要把要加载和实例化的时间跨度拉长,虽然增加了些许等待时间,却能平滑过渡到最终我们需要的画面。
具体怎么做呢。其实不复杂的,可以先获取所有需要加载的资源,放入队列中,每次加载N个(N可以根据实际情况调整),如果已经加载过的就直接通知逻辑程序实例化,不曾被加载的则调用加载程序并将调用后的加载信息(AssetBundleRequest)放入‘加载中’队列,不开携程而是用Update帧更新去判断‘加载中’队列中是否有完成的,每加载完毕一个资源先从‘加载中’队列里移除,再通知逻辑程序再进行实例化,直到队列中的请求加载完毕为止,继续下一个N个加载请求。当然这里也需要做些判断,例如已经在加载队列里的资源不重复加载等一些避免重复加载的判断。
AssetBundle的引用计数方式卸载
Assetbundle在加载后我们需要寻求释放,只有加载没有释放内存只会不断攀升。该怎么释放就成了问题,因为资源使用的地方太多,太庞杂,所以为了能更好的知道什么时候该释放资源,我们需要制定一个规则,这个在遵守这个规则的前提下,我们就知道什么时候资源没有被再使用了,有多少个地方在使用。
引用计数就是判断这种释放依据的很好的技巧,具体方式为如下:
我们对AssetBundle包装一个计数器(是个整数),当需要某个AssetBundle时先加载所有依赖的AssetBundle,每加载一个AssetBundle就为该AssetBundle的引用计数加1。
如果调用的是Prefab,会通过Instantiate进行实例化,这里必须在每次实例化时对该AssetBundle引用计数加1,不过这样在实例化时才做引用计数加1的手法,又消耗了些许我们的注意力而且容易遗漏,我们可以选择一次实例化调用一次加载,这样就节省了人额外的注意力,少一点主意力的消耗,就少一些遗漏。
如果是Texture贴图这种不需要进行实例化的资源则最好不要被再次被引用,因为被再次引用会导致引用计数的错乱,我们可以选择每次当需要Texture时通过查看AssetBundle是否加载,有则直接取,没有则加载后再取,每次取资源时都对相应的AssetBundle计数加1。
当Destroy销毁实例或者不需要用资源时,则统一调用某个自定义的Unload(假设这个接口名字是自定义类AssetBundleMrg.Unload)接口并附上加载时的关键字(为了能更快的找到AssetBundle实例),从而将对应的AssetBundle的引用计数减1。
减少引用计数后,倘若该AssetBundle引用计数为0,则认为可以进行AssetBundle卸载,则立即卸载。
但是问题又来了,及时的卸载也会有问题,因为每次都卸载后又需要该资源时需要再加载,中间消耗的IO和CPU也很多,我们可以通过增加空置倒计时时间来给卸载AssetBundle一个预留时间。
当需要卸载时,AssetBundle进入倒计时,比如5秒,5秒内仍然没有任何程序使用这个资源则立即进行卸载,如果5秒内又有程序加载该AssetBundle资源则继续使用引用计数来判断是否需要进入卸载倒计时。
不过还是有个小问题,如果大量资源在同一时间卸载,就会造成大量资源同一时间进入倒计时,倒计时完毕同时进行卸载,也会带来1帧消耗过大的问题,毕竟资源的卸载时内存的消耗,大量的内存在同一时间销毁会带来大量的CPU消耗。此时我们可以对倒计时进行随机2-5秒的时间内随机一个值,让卸载分散在这个时间段内,让卸载的消耗更加平滑。
AssetBundle的打包与颗粒度大小
Unity3D对AssetBundle的封装做的很好,当我们在打包AssetBundle时Unity3D会自动去计算AssetBundle与AssetBundle之间的依赖关系,所以我们能很轻松的将资源打的很细(贴图,网格,Shader,Prefab,每个资源分的很开)。
这使得我们能很轻松得让一个AssetBundle只装一个资源文件并且控制起来也得心应手,只要在加载时读取存有依赖关系的AssetBundle就能得到AssetBundle之间的依赖关系数据(AssetBundleManifest实例数据),根据这个依赖数据我们就能轻松的加载相关的其他AssetBundle。
既然AssetBundle颗粒度可以很容易的缩放,那么我们就需要考虑颗粒度的大小到底对项目产生多大的影响。
我们说说左右两种极端状态下的表现。
一种为颗粒度极粗状态,所有资源都打成一个AssetBundle包,所有逻辑程序要的资源都从这个AssetBundle里取。引用计数,在这里已经完全没有了用处,由于只有一个AssetBundle已经完全没有卸载的可能了。这导致了内存只会逐步增大,而绝不会因为不再需要某资源而卸载AssetBundle(当前AssetBundle的卸载机制中没有只销毁某部分资源的功能)。
我们来看看整个过程,从一个很大的文件包从网络上下载下来,解压后成为一个AssetBundle文件,然后我们读取它并从中获得资源。从这个过程来看只有一个AssetBundle的极限状态下,文件操作的数量极低,导致读取AssetBundle文件信息没有障碍,解压的IO连续性非常高,导致解压时不需要创建很多文件从IO上会相对比较快些,同时由于只有一个文件内容所以打包的压缩率也是最大的。
另一种为颗粒度极细状态,所有贴图、网格、动画、Shader、Prefab都各自打自己的一份AssetBundle(一份AssetBundle只带一个资源)。为了能更有效的控制内存,AssetBundle之间的依赖关系和引用计数在这里用处非常大。通过引用计数和依赖关系,我们能很有效的控制逻辑系统中需要的资源和内存中的资源是一致的。
我们来看看整个过程,从网上下载下来所有AssetBundle资源文件,对每个压缩过的资源文件进行解压,当需要某个资源时从AssetBundle读取资源并且读取前先根据依赖关系读取需要的资源,并且对所有加载过的AssetBundle引用计数加1。当卸载时,对当前卸载的AssetBundle引用计数减一,并且对存有需求上依赖关系的其他AssetBundle也减一(由于当前资源卸载后对其他依赖资源不再引用),如果引用计数为0则启动卸载。
我们从这个过程看来,一个极限细分颗粒度状态下的AssetBundle机制,文件操作数量会很大,IO操作的时间会因为文件增多的增大许多,导致下载时间拉长,下载完毕后解压的总时间也会拉长,打包时由于每个文件单独打包压缩因此压缩比率会降低压缩时间加长。
上述分析了两种极限状态下的利弊,我们可以根据自己项目的需求来定制AssetBundle打包机制。