关于Unity加载优化,你可能遇到这些问题

关于Unity加载优化,你可能遇到这些问题

关键字

资源加载、卸载
Instantiate 实例化
资源管理方式


一、资源加载

Q1:将Shader独立打包,如果我在启动游戏的时候加载一次,那么之后切换场景是不是就不用每次都加载了?

确切地说,要达到后续Shader都不出现加载开销,需要满足以下两个条件:
(1)包含Shader的AssetBundle文件常驻内存;
(2)Shader已经全Load加载好;
只要满足这两个条件,后续加载好的GameObject,但凡依赖于这些Shader的,都会直接拿来进行使用,而不会再有加载和解析开销。

Q2:假设有两个界面Panel A和Panel B,它们均依赖一个共享Atlas C。AssetBundle打包时,如果我们将其分别打包的话,那么在使用时是否不能将AssetBundle C进行卸载?如果在加载Panel A和B之前,将Atlas C加载到内存中,但是将AssetBundle C进行卸载,那么在加载和实例化Panel A和B时,是否会出问题?

是的,不仅是Panel和Atlas,只要是A或B依赖于C,就必须保证在加载或实例化A或B时,C的AssetBundle一定存在于内存中。如果之前将C对应的AssetBundle进行卸载,则加载或实例化A和B时,引擎将无法自动将C绑定给A和B进行使用。
关于AssetBundle依赖性打包,我们已在之前的分享【揭开AssetBundle庐山真面目】中有所提及。

Q3:请问这个Loading.UpdatePreloading是什么东西,为什么会突然那么高?一般情况下有没有什么优化的办法?
请输入图片描述

这是Unity引擎最主要的加载函数。该项一般在切换场景时或主动动态加载资源时较大。 一般来说,加载资源越多、越复杂,则其反映的Loading.UpdatePreloading耗时则越大。
优化之前,必须先定位该函数的CPU占用瓶颈。下图则为我们的案例项目,Loading.UpdatePreloading函数在UWA测评报告中的总体CPU分配情况。通过这个堆栈信息,开发团队就可以对函数的耗时分配一目了然,从而有的放矢地进行优化。
请输入图片描述

Q4:关于Loading.ReadObject耗费比较高,有什么推荐的方法吗?

Loading.ReadObject是Unity引擎的资源加载函数,一般出现在切换场景和加载API调用时,这其中包括纹理、网格、Material、Shader、AnimationClip等资源。如果你发现该值过高,建议去大力优化加载的相关资源。对于每种资源的加载,我们正在以专题的方式进行总结和分享,建议大家可以先查看以下内容:
加载模块之纹理
加载模块之网格
加载模块之Shader
加载模块之动画片段

Q5:我使用Shader.WarmupAllShaders操作,在后续加载资源还是有CreateGPUProgram出现。(Shader都在一个AssetBundle文件中,都是常驻内存的,不会删掉)是必须使用ShaderVariantCollection来加载Shader吗?

以上这种问题的较大可能是:Shader被打包到不同AssetBundle中了,WarmupAllShaders仅能对当前内存中的Shader进行warm up。后续如果又有Shader加载进来,则仍然会出现CreateGPUProgram操作。

建议开发团队使用UWA的资源检测工具,检测下AssetBundle中Shader的具体打包情况,查看是否出现了冗余打包的问题。

Q6:我们在UWA上进行了性能测试,发现安卓上同步加载AssetBundle资源会非常耗CPU, 所以近期对资源加载方式做了比较大的调整,绝大部分的资源使用异步加载的形式。 不过这里有个疑问想咨询下: AssetBundle.LoadAsync 和 WWW加载方式都可以用来异步加载AssetBundle, 但是两者API特点也不同, 目前看WWW更耗内存一些, 请问这两种方式更建议使用哪一种 ?

AssetBundle.LoadAsync 是在获取了 AssetBundle 对象之后加载其中的资源的;而 WWW 加载是在获取 AssetBundle,两者的作用是不同的。
开发者可能是希望了解异步加载 AssetBundle 的几个 API 之间的区别,相关的接口如下:

new WWW 
WWW.LoadFromCacheOrDownload

这两种方式的具体区别可先参考《你应该知道的AssetBundle管理机制》中的“AssetBundle加载进阶”部分。


二、资源卸载

Q1:切换场景时,卸载上一个场景总感觉耗时,请问大家是否有什么推荐的方案?
请输入图片描述

这是Unity在切换场景时调用Resources.UnloadUnusedAssets这个函数造成的,通常情况下其开销都是会比较大。建议开发团队通过UWA性能检测,在加载模块中进一步定位卸载场景的元凶。
请输入图片描述

Q2:Resources.UnloadUnusedAssets() 不仅局限于Resources.Load的资源,还包括AssetBundle.Load的资源,是吗?

是的,Resources.UnloadUnusedAssets同样可以卸载由AssetBundle.Load加载的资源,只是前提是其对应的AssetBundle已经调用Unload(false),且并没有被引用。

Q3:我用UGUI做的一个界面中有一个背景图片,游戏中没有做任何处理,关闭销毁这个界面后这个图片还在内存中,但是我已经调用过了Resources.UnloadUnusedAssets,如下图所示。我的预设没有打成AssetBundle,是放在Resource路径加载的。请问是什么原因导致的呢?
请输入图片描述
请输入图片描述

在使用 Resources.Load 加载 UI 界面的情况下,即使“关闭销毁这个界面”后,Resources.UnloadUnusedAssets 确实还是无法卸载对应的图集的。因为此时该图集依然被 Resources.Load 加载出来的 Prefab 引用。
对于这种情况,我们的建议是手动调用 Resources.UnloadAssets 来手动释放图集(可以通过 Sprite.texture 来找到对应的图集),在重新实例化该 UI 界面时,图集也会自动进行 Reload 的。

Q4:用Resources.UnloadAsset 释放未Instance的Object 会出现这样的错误 :Unload Assets may only be used on individual assets and can not be used on GameObject's/Components or AssetBundles.请问该如何解决?

Resources.UnloadAsset仅能释放非GameObject和Component的资源,比如Texture、Mesh等真正的资源。对于由Prefab加载出来的Object或Component,则不能通过该函数来进行释放。

Q5:我在Profiler中看到 GC.MarkDependencies 的CPU消耗有900ms+, 虽然在退出战斗的时候调用了Resources.UnloadUnusedAssets(); 可是卡顿还是很明显,请问有什么推荐的方案吗?

GC.MarkDependencies的消耗是由Resources.UnloadUnusedAssets引起的。该函数的主要作用是查找并卸载不再使用的资源。游戏场景越复杂、资源越多,该函数的开销越大,一般在300~2000 ms范围内。所以,我们在UWA报告中加入了对该函数的调用监测,以便让大家更好地掌控它的调用情况。
请输入图片描述
对于该函数的优化,我们建议一方面控制场景中不必要的资源量,同时通过UnloadAsset来及时卸载不再使用的资源,以减少Resources.UnloadUnusedAssets的耗时压力。
后续,我们会在加载模块的相关文章中为大家详细讲解Resources.UnloadUnusedAssets的性能问题,敬请期待。

Q6:我有一个UI预设,它使用了一个图集, 我在打包的时候把图集和UI一起打成了AssetBundle。我在加载生成了GameObject后立刻卸载了AssetBundle对象, 但是当我后面再销毁GameObject的时候发现图集依然存在,这是什么情况呢?

这是很可能出现的。unload(false)卸载AssetBundle并不会销毁其加载的资源 ,是必须调用 Resources.UnloadUnusedAssets才行。关于AssetBundle加载的详细解释可以参考我们之前的文章:你应该知道的AssetBundle管理机制。

Q7:如果先Destroy Prefab ,然后将Prefab中用到的AssetBundle再进行Unload,这样的顺序是否会有问题 ? 我在手机上测试时发现这样做内存中就会一直存在,不释放;如果反过来, 就可以释放。另外,我是在Destroy 的时候调用的Resources.UnloadUnusedAssets();,请问这会影响最终的结果吗?

确实可能发生这种情况,在 Resources.UnloadUnusedAssets(); 时,如果还没有进行AssetBunlde的Unload 操作,那么从AssetBunlde中加载的资源依然会因为被AssetBunlde引用而无法被卸载。 开发团队可以尝试 Destory 后做 AssetBunlde的Unload,最后进行 Resources.UnloadUnusedAssets(); 。


三、管理方式

Q1:现在Unity还不能将场景和 NavMesh数据或者Lightmap数据分离,是吗?我是想先加载一个干净的场景,然后再动态切入不同的光照贴图和NavMesh网格数据,有什么办法吗?

Lightmap是可以与场景分离的,并且可以通过AssetBundle进行动态加载。建议将Lightmap作为普通的资源进行打包,动态加载后,通过LightmapSetting接口整体替换场景的Lightmap。目前,Navmesh确实是无法与场景分离的。但是在Unity 5.x版本以后,引擎已经允许通过LoadLevelAdditive加载多个场景的方式来加载NavMesh,因此,研发团队可以尝试预先将NavMesh在多个场景内烘焙好,然后通过LoadLevelAddtive或类似API来进行加载,从而达到动态加载NavMesh的效果。

Q2:从点击应用到出现游戏画面,这个时间长短是不是和Resource目录大小有关系?

从点击应用到首次出现应用画面,其加载时间主要与两方面相关:
1、Resources文件夹中的资源数量。在游戏启动时,Unity引擎会为Resources文件夹下的资源建立一个查找树来存放与其对应的索引,便于后续资源的加载。一般来说,Resouces文件夹下资源数量越多,其构建时间越长,应用的启动也就越慢;
2、首场景的资源加载和相关代码的初始化工作。如果首场景的资源量较多,其脚本初始化的任务较重,则应用的启动时间也会越慢。

Q3:AssetBundle在使用时解压出来的资源会占用一定的内存。我们现在想尝试使用两种加载方式:(1)在AssetBundle加载相关的资源后,将资源进行缓存,并卸载AssetBundle文件;(2)对AssetBundle文件进行缓存,以后用到相关资源后再进行直接进行加载。请问这两种方式你们推荐哪一种比较好?

对于Unity 5.3版本之前的项目,建议通过LoadFromCacheOrDownload或LoadFromFile的方式来加载AssetBundle,这样既可以降低Assetbundle对象的内存占用,又可以保持AssetBundle与资源之间的链接关系,从而方便后续具有依赖关系的Prefab的加载。

对于Unity 5.3版本之后的项目,可以从AssetBundle打包本身进行处理。即:使用LZ4格式的AssetBundle文件,而不使用默认的LZMA格式。同样可以达到上述的需求,稍有不足的是,LZ4格式的AssetBundle文件较之LZMA格式的文件占用稍大。

如果是依赖关系打包,对于被依赖的共享资源AssetBundle文件,我们还是建议将其缓存在内存中,虽有一定的内存增加,但可以通过上述办法极大地降低内存上的占用,缓解内存压力。

Q4:怎样动态加载Navmesh?

目前Navmesh不支持动态加载,只能随场景一起加载, 因此可以考虑将带有Navmesh的场景打包成AssetBundle,然后使用LoadLevel加载AssetBundle中的场景。
Navmesh的动态加载已经在Unity的Roadmap中。而当前,Navmesh是和场景绑定的,也就是说目前只能通过LoadLevel(不支持> > LoadLevelAdditive的加载方式)来加载场景的同时,自动加载对应的Navmesh数据。替代方案是:将多个“场景Prefab”的Navmesh 合并到同一个场景中烘焙好(互不重叠),然后再将这些“场景Prefab”分离到各个单独的场景中去;在运行阶段,Navmesh随着第一个场景一次性加载,而对于其他的场景物件,再通过LoadLevelAdditive来加载对应的场景即可。缺点是为了使场景物件对齐Navmesh,在场景制作时就不能出现坐标上的重叠,因此仅供参考。
在Unity 5.x下,Lightmap的动态加载,需要通过脚本将烘焙时每个物件的Lightmapindex和Lightmapscaleoffset记录下,并在运行时动态加载后设置回去的方式来实现。因为目前Lightmapindex和Lightmapscaleoffset信息是和场景绑定在一起,储存在Lightmapsnap.assets 中,发布时也是放在场景信息中,因此不会记录在Prefab 上。

Q5:同一个纹理,有多个Prefab生成的实例,会有多份这个纹理的copy吗?

该问题需要查看 Prefab 的具体加载方式。如果仅是通过 Resources.Load 进行加载,那么纹理是不会存在多份的,但如果是通过AssetBundle加载,每个 Prefab 均为一个 AssetBundle 且纹理没有进行依赖关系打包的话,那么纹理资源确实会在内存中存在多份。如果你发现了内存中存在多份相同纹理,且是通过 AssetBundle 文件来加载资源的,则建议将 AssetBundle 上传到UWA网站(www.uwa4d.com)中,其资源检测工具能协助开发团队高效检测并定位AssetBundle中的冗余资源。


四、Instantiate 实例化

Q1:我在第一次执行GameObject.Instantiate一些资源的时候会卡(当时加载当时就实例的情况),有的复杂资源甚至在第一次GameObject.Instantiate的时候会卡70多毫秒,造成明显的卡顿,请问有什么好的解决方案吗?

Instantiate的卡顿与三部分开销相关:相关资源加载、脚本组件的序列化和构造函数的执行,并且绝大部分原因均是相关资源加载导致。所以,我们的建议如下:
1、通过 Profiler 查看 Instantiate 具体的CPU分配情况;
2、如果是资源加载导致的性能瓶颈,则一方面通过简化资源来缓解CPU耗时压力,另一方面通过 AssetBundle 依赖关系打包将资源预先加载,即将此处 Instantiate 的总体耗时拆分,平摊到之前帧进行执行(比如切换场景处等),从而让 Instantiate 实例化操作的局部耗时更加平滑;
3、如果是脚本组件序列化导致的性能瓶颈,则可尝试减少脚本中的序列化信息;
4、如果是构造函数的执行导致的性能瓶颈,一般只能在策略上进行规避,比如降低 Instantiate 的调用频率等。
针对资源加载部分,我们近期正以专题的形式连载中,后续会对其Instantiate实例化的调用进行更为详细的分析,敬请期待。
加载模块之纹理
加载模块之网格
加载模块之Shader
加载模块之动画片段