Unity移动端游戏性能优化简谱之 常见游戏内存控制
《Unity移动端游戏性能优化简谱》从Unity移动端游戏优化的一些基础讨论出发,例举和分析了近几年基于Unity开发的移动端游戏项目中最为常见的部分性能问题,并展示了如何使用UWA的性能检测工具确定和解决这些问题。内容包括了性能优化的基本逻辑、UWA性能检测工具和常见性能问题,希望能提供给Unity开发者更多高效的研发方法和实战经验。今天向大家介绍文章第二部分:资源内存、Mono堆内存等常见游戏内存控制,共13小节,包含了纹理资源、网格资源、动画资源、音频资源、材质资源等多个资源内存以及Mono堆内存等常见的游戏内存控制讲解。
(全文长约11400字,预计阅读时间约20分钟)
文章第一部分《Unity移动端游戏性能优化简谱之 前言》可戳此回顾,完整内容可前往UWA学堂查看。
1. 总览
1.1 概念解释
首先,在讨论内存相关的各项参数和制定标准之前,我们需要先理清在各种性能工具的统计数据中常出现的各种内存参数的实际含义。
在安卓系统中,我们最常见到和关心的PSS(Proportional Set Size)内存,其含义为一个进程在RAM中实际使用的空间地址大小,即实际使用的物理内存。就结果而言,当一个游戏进程中PSS内存峰值越高、占当前硬件的总物理内存的比例越高,则该游戏进程被系统杀死(闪退)的概率也就越高。
而在PSS内存中,除了Unused部分外,我们一般比较关心Reserved Total内存和Lua、Native代码、插件等系统缓存、第三方库的自身分配等内存。Reserved Total占比一般较高,故其大小和走势,也是UWA性能分析工具的主要统计对象(对于使用到Lua的项目,UWA另外提供了Lua专项测试报告统计Lua内存,下文还会提到)。
Reserved Total和Used Total为Unity引擎在内存方面的总体分配量和总体使用量。 一般来说,引擎在分配内存时并不是向操作系统 “即拿即用”,而是首先获取一定量的连续内存,然后供自己内部使用,待空余内存不够时,引擎才会向系统再次申请一定量的连续内存进行使用。
注意:对于绝大多数平台而言,Reserved Total内存 = Reserved Unity内存 + GFX内存 + FMOD内存 + Mono内存
(1) Reserved Unity内存
Reserved Unity和Used Unity为Unity引擎自身各个模块内部的内存分配,包括各个Manager的内存占用、序列化信息的内存占用和部分资源的内存占用等等。
通过针对大量项目的深度分析,UWA发现导致Reserved Unity内存分配较大的原因主要有以下几种:
序列化信息内存占用:Unity引擎的序列化信息种类繁多,其中最为常见且内存占用较大的为SerializedFile。该序列化信息的内存分配主要是项目通过特定API(WWW.LoadFromCacheOrDownload、CreateFromFile等)加载AssetBundle文件所致。
资源内存占用:主要包括Mesh、AnimationClip、RenderTexture等资源。对于未开启“Read/Write Enable” 选项的Mesh资源,其内存占用是统计在GFX内存中供GPU使用的,但开启该选项后,网格数据会在Reserved Unity中保留一份,便于项目在运行时对Mesh数据进行实时的编辑和修改。同时,如果研发团队同样开启了纹理资源的 “Read/Write Enable” 选项(默认情况下为关闭),则纹理资源同样会在Reserved Unity中保留一份,进而造成其更大的内存占用。
(2) GFX内存
GFX内存为底层显卡驱动所反馈的内存分配量,该内存分配由底层显卡驱动所控制。一般来说,该部分内存占用主要由渲染相关的资源量所决定,包括纹理资源、Mesh资源、Shader资源传向GPU的部分,以及解析这些资源的相关库所分配的内存等。
(3) 托管堆内存
托管堆内存表示项目运行时代码分配的托管堆内存分配量。对于使用Mono进行代码编译的项目,其托管堆内存主要由Mono分配和管理;对于使用IL2CPP进行代码编译的项目,其托管堆内存主要由Unity自身分配和管理。
1.2 内存参数标准
在我们了解了内存相关的各项参数的含义之后,知道了避免游戏闪退的重点在于控制PSS内存峰值。而PSS内存的大头又在于Reserved Total中的资源内存和Mono堆内存。对于使用Lua的项目来说,还应关注Lua内存。
根据UWA的经验,只有当PSS内存峰值控制在硬件总内存的0.5-0.6倍以下的时候,闪退风险才较低。举例而言,对于2GB的设备而言,PSS内存应控制在1GB以下为最佳,3GB的设备则应控制在1.5GB以下。
而对于大多数项目而言,PSS内存大约高于Reserved Total 200MB-300MB左右,故2GB设备的Reserved Total应控制在700MB以下、3GB设备则控制在1GB以下。
特别的,UWA还认为Mono堆内存需要予以关注,因为在很多项目中,Mono堆内存除了存在本身驻留偏高或存在泄露风险的问题外,其大小还会影响GC耗时。UWA认为控制在80MB以下为最佳。
下表为UWA提供的细化到每一种资源内存的推荐标准,制定较为严格。不过,仍需要开发者根据自身项目的实际情况予以调整。比如某个2D项目节省了几乎所有网格资源的使用,那么其他资源的标准就可以放宽很多。
关于更多的细化标准,大家可以直接在UWA线上产品中进行对应查看。
基于项目实情制定内存标准后,一般需进一步与美术、策划协商,给出合理的美术规范参数,并撰写成文档。
定好规范后,定时检查项目里的所有美术资源是否符合规范,及时修改和更新。检查美术是否合规的过程,可以利用Unity提供的回调函数写自动化工具,提高效率。可以参考《自动化规范Unity资源的实践》。
如果资源若不能批量处理成高中低配版本,就需要美术为各个画质等级制作不同的资源。
1.3 本地资源检测服务-项目资源检测
各项资源内存的引擎设置项繁琐且并不都能在运行时被采集,下文即将提到的内容虽然是众多项目中常见且重要的问题,但实际项目中的情况更加复杂。通过本地资源检测服务的项目资源检测界面,往往能发现更多资源设置项的问题。它们不光影响相关资源的内存占用,还会根据情况对CPU耗时和GPU造成不同程度的压力。
为此UWA根据经验设计了检测规则和阈值,以此为依据采集和统计了存在这些问题的资源,并给出了对应的优化建议,帮助开发者针对资源进行更加深入的排查和优化。
2. 常见的共通性问题
这一部分提到的问题没有特定性,不仅仅出现在一种资源内存中。所以,为了避免赘述,此处统一予以讨论。
2.1 疑似冗余现象
在UWA GOT Online Resource模式报告的具体资源列表(下文简称资源列表)中,我们常能看到某一项资源的数量峰值大于1且被标红。数量峰值同样是资源使用中非常重要的一项指标。所谓 “数量峰值”,是指同一资源在同一帧中出现的最大数量。理论上,数量峰值这一参数不应大于1,当数量峰值大于1时,列表中会将其标红,我们称之为疑似冗余资源。
一般情况下,出现这种问题是由AssetBundle资源加载导致的,即在制作AssetBundle文件时,部分共享资源(比如Texture、Mesh等)被同时打入到多份不同的AssetBundle文件中但没有进行依赖打包,从而当加载这些AssetBundle时,内存中出现了多份同样的资源,即资源冗余,建议对其进行严格的检测和完善。
针对排查出的疑似冗余现象,可以使用UWA在线AssetBundle检测工具排查是否确实存在AssetBundle冗余的问题,尽量减少AssetBundle的冗余。建议根据冗余资源的内存大小来决定对冗余问题的优化优先级。
值得一提的是,所谓 “疑似冗余资源”,是指在检测过程中,我们尝试搜索项目运行时的冗余资源并将其反馈给用户。但是,我们并无法保证该项检测的100%正确性。这是因为,我们判断的标准是根据资源的名称、内存占用等属性(因资源类型不同可能有格式、Read/Write、时长等属性,以报告资源列表中呈现的属性为准)而定,当两个资源的名称、内存占用等属性均一致时,我们认为这两个资源可能为同一资源,即其中一个为 “冗余” 资源。但项目中确实也存在资源不同但各项属性都相同的情况。因此,我们将通过以上规则提取出的资源归为 “疑似冗余资源”。所以,是否确实为冗余资源,还需要结合项目实情和在线AssetBundle检测报告才能下结论。
2.2 未命名资源
在资源列表中,有时发现存在资源名称为N/A的资源。一般来说名为N/A的资源都是在代码中new出来但是没有予以命名的。建议通过.name方法对这些资源进行命名,方便资源统计和管理。尤其是其中冗余比较严重的或者个别内存占用非常大的N/A资源应予以关注和严格排查。
2.3 常驻资源内存占用大
在资源列表中,有时结合资源的生命周期曲线发现,一部分本身内存占用较大的资源在被加载进内存后,驻留在内存中,直到测试流程结束都没有被卸载,可能造成越到游戏后期资源内存占用越大、峰值越高。建议排查这些资源是否有常驻在内存中的必要。如果不再需要被使用,则应检查为什么场景切换时没有卸载;对于持续时间久的单场景中持续驻留的资源,则可以考虑手动卸载。
对于资源是否常驻的考量涉及内存压力和CPU耗时压力之间的取舍。简单来说,如果当前项目内存压力较大,而场景切换时的CPU耗时压力较小,则可以考虑改变缓存策略,在场景切换时及时卸载下一个场景用不到的资源,在需要时再重新加载。
3. 纹理资源
3.1 纹理格式
纹理格式设置不合理通常是造成纹理资源占据较大内存的主要原因之一。即便是对于很多已经建立过美术资源标准并统一修改过纹理格式的项目而言,仍然很容易统计到存在大量的RGBA32、ARGB32、RGBA Half、RGB24等格式的纹理资源。这些格式的纹理不但内存占用较大,还会导致游戏包体较大、加载这些资源的耗时较高、纹理带宽较高等等问题。
出现这类问题的原因主要有以下几种:存在一些“漏网之鱼”,比如美术命名不规范导致没有被回调函数修改,或者是代码中创建的资源没有设置其纹理格式;硬件或纹理资源本身不支持目标格式纹理,导致被解析为未压缩格式的纹理。
对于前一种情况,在资源列表中发现有问题的资源后,需要回到项目中自行排查修改;对于后一种情况,UWA推荐的硬件支持的纹理格式主要有ASTC和ETC2。
其中ETC2格式需要对应的纹理分辨率为4的倍数,在对应的纹理开启了Mipmap时更是严格要求其分辨率为2的次幂。否则,该纹理将被解析成未压缩格式。
3.2 分辨率
纹理资源的分辨率(即资源列表中的长度和宽度参数)同样也是造成内存占用过大的主要原因。一般来说,分辨率越高,其内存占用则越大。其中最为需要关注的是占据较大分辨率(一般为 ≥ 1024)的纹理。对于移动平台来说,过于精细的表现通过玩家的肉眼很难分辨出差异,而过大的分辨率往往意味着不必要的浪费。
在不同档位的机型上使用不同分辨率大小的纹理资源是非常实用且易操作的分级策略。这一点即便对于图集纹理也同样适用,特别地,Unity针对SpriteAltas提供了Variant功能,可以快捷的复制一份原图集并根据Scale参数降低该变体图集的分辨率,以供较低的分级使用。
3.3 Read/Write Enabled
上文提到过,纹理资源的内存占用是计算在GFX内存中的,也就是传向GPU端的部分。而开启Read/Write Enabled选项的纹理资源还会保留一份内存在CPU端,从而造成该资源内存占用翻倍。
UWA GOT Online Resource模式报告资源列表或是本地资源检测报告中都直接展示了哪些纹理开启了Read/Write Enabled选项。实际上,不需要在运行时进行修改的资源是不需要开启Read/Write Enabled选项的,开发者应排查并关闭不必要的设置从而降低内存开销。
3.4 Mipmap
当一张纹理开启Mipmap时,它的内存占用会上升为原始数据的1.33倍。对于3D对象,比如场景中的地形、物件或人物,其纹理的Mipmap功能是建议开启的,可以在运行时降低带宽。但值得注意的是,在真人真机测试报告中的Mipmap页面中,统计了游戏过程中开启Mipmap纹理的各个Mipmap通道的屏占比变化趋势。如果场景中的3D物体大面积地使用1/2乃至1/4、1/8的Mipmap通道,说明该3D物体使用的纹理分辨率偏高,存在浪费现象。可以改用更低分辨率的纹理。
但如果是2D项目或UI界面资源,则建议将对应纹理的Mipmap功能关闭,从而避免不必要的内存开销。
3.5 各向异性与三线性过滤
开启纹理的各向异性滤波有利于地面等物体的显示效果,但会导致GPU渲染带宽上升。其中的原理是,纹理压缩采样时会去读缓存里面的信息,如果没读到就会往离GPU更远的地方去读System Memory,因此所花的时钟周期也就会增多。当开启各向异性导致采样点增多的时候,发生Cache Miss的概率就会变大,从而导致带宽上升的更多。在引擎中可以通过脚本关闭纹理资源的各向异性;或者对于需要开启各向异性的纹理,引擎中可以设置其采样次数为1-16,也建议尽量设为较低的值。
将纹理设置为三线性过滤,纹理会在不同的Mipmap通道之间进行模糊,相比双线性过滤GPU渲染带宽将会上升。三线性插值采8个采样点(双线性采4个采样点),同样会使Cache Miss的概率变大,从而导致带宽上升,应尽量避免使用三线性过滤。
这两种采样方式的纹理也会被UWA的本地资源检测统计和罗列出来,供开发者排查。
3.6 图集制作
图集制作不够科学也是项目中常会发生的问题。资源列表中有时会出现数量峰值较高的图集纹理,但不一定是冗余。一种情况是,大量小图被打包到同一图集中,导致该图集纹理资源设置的最大分辨率(比如2048*2048)一张装不下这么多小图,该资源就会生成更多的纹理分页来打包这些小图。因此,只要游戏过程中依赖某一张纹理分页中的某一张小图,就会将该资源、也即该资源下所有的分页都全部加载进内存中,从而造成不必要的浪费。所以一般建议控制到2-3张分页以内较为合理。
即便不出现上述这个较为极端的现象,很多项目中也会出现“牵一发而动全身”的现象。即明明只用图集中的一张或几张小图,却将内存占用颇大的整个纹理都加载进了内存。
为此,在制作打包图集时,严格按照小图的使用场景、分类进行打包是非常重要的策略。选用合适的分辨率从而避免纹理没有被填充满而导致浪费,也是开发者需要注意的点。
3.7 使用TextMeshPro的情况
TextMeshPro能为UI组件提供更好的表现和便利的功能,使得其受到不少开发者的青睐。但使用TMP而产生的TMP字体图集纹理(名称中带有SDF Atlas,格式为Alpha 8的纹理)也有一些坑值得注意。
(1)有时,结合字体资源列表注意到内存中还存在TMP图集纹理对应的.ttf字体文件。说明该TMP字体图集为动态字体。可以考虑在项目开发结束、确保游戏要用到的字符都已添加到动态字体的Altas纹理中后,将动态TMP重新设置为静态TMP,并且解除对.ttf文件的依赖。这样一来,对应的字体资源将不会出现在内存中。不过,如果这种字体还被用作用户输入,则不建议采用此方法。
(2)Atlas字体纹理的分辨率较大。此时建议在引擎中排查字符有没有填满图集纹理,纹理的制作生成是否合理。对于动态TMP,如果没有填满,如只占据了纹理的3/4不到,则可以考虑开启Multi Atlas Textures选项,并设置纹理大小,举例而言就可以使1张40964096的纹理变为3张20482048的纹理,节省32MB-3*8MB=8MB的空间。
(3)资源列表中有TMP相关的资源(LiberationSans SDF Atlas、EmojiOne),它们都是TMP的默认设置,可以在Project Settings-TextMesh Pro Settings中解除对这些默认资源的依赖,就不会出现在内存中了。
由于Multi Atlas Textures是动态TMP的选项,所以(1)、(2)无法同时使用,可以根据项目实情酌情选用。
3.8 使用本地资源检测排查纹理问题
在本地资源检测中包含了“使用非压缩格式的纹理”、“尺寸过大的纹理”、“开启Read/Write选项的纹理”、“开启Mipmap选项的Sprite纹理”、“开启各向异性过滤的纹理”、“过滤模式为Trilinear的纹理”等上文已经提及的检测规则,方便开发者精确定位存在潜在性能问题的纹理资源。
4. 网格资源
4.1 顶点和面片数
顶点和三角形面片数过多的网格资源不仅会造成较高的内存占用,同时也不利于裁剪,容易增加渲染面数,在渲染时对GPU和CPU造成压力。针对这些网格,一方面可以简化网格,减少顶点数和面数,制作低模版本,供中低端机型分级使用;而另一方面针对单个顶点数过高的静态网格,比如一些复杂的地形和建筑,可以考虑拆分成若干个重复的小网格重新拼接。只要做好合批操作,就能以付出一点Culling计算耗时为代价,减少同屏渲染面片数。
4.2 顶点属性
如果没有统一美术资源标准且在导入时没有进行处理,则项目中的网格很有可能包含大量“多余”的顶点数据。这里的“多余”数据是指网格数据中包含了渲染时Shader中所不需要的数据。举例而言,如果网格数据中含有Position、UV、Normal、Color、Tangent等顶点数据,但其渲染所用的Shader中仅需要Position、UV和Normal,则网格数据中的Color和Tangent则为“多余”数据,从而造成不必要的内存浪费。其中,一个小网格资源带有顶点属性,会使所在的Combined Mesh也带有顶点属性,需要予以注意。
针对这个问题,一个比较简单的方法是,尝试开启“Optimize Mesh Data”选项。该选项位于Player Setting的Other Settings中。勾选后,引擎会在发布时遍历所有的网格数据,将其“多余”数据进行去除,从而降低其数据量大小。但是,需要注意的是,对于在Runtime情况下有修改Material需求的网格,建议研发团队对其进行额外的注意。如果Runtime时需要为某一个GameObject修改更为复杂、需要访问更多顶点属性的Material,则建议先将这些Material挂载在相应的Prefab上再进行发布,以免引擎去除Runtime中会进行使用的网格数据。
4.3 Read/Write Enabled
在资源列表中,常常统计到大量顶点属性不显示为-1(或“-”)的网格资源。只有网格资源开启Read/Write时,UWA报告才能采集到顶点属性信息。此时,顶点属性不显示为-1,且会使得网格占用内存上升。一般而言,不需要在CPU端进行修改的网格是不需要开启Read/Write的。可以在编辑器中通过API修改这些网格的Read/Write属性,或者对于FBX中的网格可以直接在Inspector窗口中修改。
4.4 使用本地资源检测排查网格问题
在本地资源检测中包含了“面片数过大的网格”、“包含Color属性的网格”、“包含Tangent属性的网格”、“包含Normal属性的网格”、“包含UV3或UV4属性的网格”、“开启Read/Write选项的网格”等上文已经提及的检测规则,方便开发者精确定位存在潜在性能问题的网格资源。
5. 动画资源
一般来说,内存占用大于200KB,且时长较短的动画资源就可以被认为是内存占用偏大的动画资源,有一定的优化空间。针对动画资源的优化方法有:
(1)将Animation Type改成Generic。相比另一种Legacy类型,Generic实际上使用了Unity新版的Mecanim动画系统,整体性能要好很多,一般不建议使用老版的动画系统,而第三种Humanoid同样是新版动画系统提供给人性角色的特殊工作流,具有灵活复用性的优点,但对模型的骨骼数量有要求(即人形骨骼),可以根据项目需要选用。
(2)将Anim. Compression改成Optimal。Optimal实际上就是让Unity在数个算法中自动选择最优的曲线表达方式,从而占用最小的存储空间。而Keyframe Reduction则是一个相对稳定保守的算法,对动画的表现效果产生影响的概率更小。
(3)关闭Resample Curves选项。官方文档中称开启该选项会有一定的性能提升,但事实上根据《自动化规范Unity资源的实践》中的说法,上文提到的开启Resample Curves的性能提升体现在播放时而非加载时、且效果微乎其微;反倒是还可能造成错误的动画表现。所以结合实验数据,大部分情况下,这个选项是建议关闭的。
(4)考虑使用API剔除动画资源的Scale曲线和压缩动画的精度。其中,压缩动画精度的做法可以参考《Unity动画文件优化探究》。
以上四种方法都可以有效降低动画资源的内存占用,但(2)、(4)两种理论上会造成动画精度的损失,但不一定会看得出来。建议研发团队自行调试,在确保动画表现不受影响的情况下尽量优化其内存占用。
在本地资源检测中包含了“Compression != Optimal的动画资源”、“动画的导入设置未关闭ResampleCurve”、“包含Scale曲线的动画片段”、“精度过高的动画片段”等上文已经提及的检测规则,方便开发者精确定位存在潜在性能问题的动画资源。
6. 音频资源
对于时长较长的BGM和一些常规的时长较短但内存大的音频资源,有一定的优化空间。针对音频资源的优化方法有:
(1)开启Force To Mono。开启音频资源的Force To Mono会使音频被自动混合为单声道,而并非丢失一个声道,从而在对表现效果影响较小的前提下大幅降低音频内存。
(2)修改其加载方式(Load Type)为Compressed In Memory或Streaming。Compressed In Memory适用于大部分常规音频,而Streaming则适合时常较长且内存占用大的背景音乐。
(3)对于Compressed In Memory的音频,修改其压缩格式(Compression Format)为压缩率更大的格式,如Vorbis、MP3;
(4)对于Vorbis、MP3压缩格式的音频,还可以继续调低其Quality参数,进一步压缩其内存。
以上方式都可以有效减少音频资源内存(其中Streaming可以稳定降至200KB左右),但会造成一定的耗时代价或音质降低,可以酌情选用。
在本地资源检测中包含了“双声道的音频”、“未使用Streaming加载的长音频”、“该音频中使用了Quality过高的Vorbis与MP3压缩”等上文已经提及的检测规则,方便开发者精确定位存在潜在性能问题的音频资源。
7. 材质资源
材质资源本身内存占用较小,我们一般更加关注如何优化其数量,因为它的数量过多会影响之后会提到的Resource.UnloadUnusedAssets API的耗时。
材质资源数量过多,往往主要是因为Instance类型的冗余Material资源过多。一般来说,该种情况的出现是因为通过代码访问并修改了meshrender.material的参数,因此Unity引擎会实例一份新的Material来达到效果,进而造成内存上的冗余。对此,建议通过MaterialPropertyBlock的方式来进行优化,具体相关操作和例子见如下文章《使用MaterialPropertyBlock来替换Material属性操作》。不过这种方法在URP下不适用,会打断SRP Batcher。除此之外,则需要关注和优化非Instance的材质资源的疑似冗余现象,不再赘述。
除了数量上的问题外,材质资源往往还涉及到一些纹理采样和Shader使用相关的问题,导致一些额外的内存和GPU性能浪费,而其中比较值得关注的也已经作为检测规则统计在UWA本地资源检测报告中。
对于使用纯色纹理采样的材质,可以将纹理采样替换为一个颜色参数,从而节省一张纹理采样的开销;而对于空纹理采样的材质,Unity会采样内置提供的纹理,但是计算得到的颜色是一个常数,仍然属于浪费;又对于包含无用纹理采样的材质,由于Unity的机制,材质球会自动保存其上的纹理采样,即使更换Shader也不会把原来依赖的纹理去除,所以可能会造成误依赖实际不需要的纹理带进包体的情况,从而造成内存的浪费。
8. Render Texture
8.1 渲染分辨率
资源列表中的一些RT资源能反映项目当前的渲染分辨率。对于GPU和渲染模块压力较大的项目,在中低端机型上降低其渲染分辨率是非常直观有效的分级策略。一般低端机型上可以考虑不采用真机分辨率,降到0.8-0.9倍,甚至很多团队会选择0.7倍或720P。
如果一些其他的RT资源分辨率过高也应引起注意,尤其是2048*2048以上的资源。应当排查是否有必要用到如此精细的RT,在低端机上考虑采用更低分辨率的效果。
8.2 抗锯齿
资源列表中展示了RT资源的AA倍数。开启多倍AA会使RT占用内存成倍上升,并对GPU造成压力。建议排查是否有必要开启AA,尤其在中低端机上,可以考虑关闭此效果。
特别的是,在华为部分机型上2倍的AA会失效。即已经造成了性能消耗但没有实际起到抗锯齿效果。
8.3 后处理
一些常见的后处理相关的RT(如Bloom、Blur)是从1/2渲染分辨率开始采样,可以考虑改从1/4开始采样、并减少下采样次数,从而节省内存并降低后处理对渲染的压力。
站在性能优化的角度,在中低端机型上甚至最好完全关闭各类后处理。围绕一些常见后处理效果的讨论会在下文GPU部分中进一步展开。
8.4 URP下的RT
使用URP时,内存中会多出_CameraColorTexture和_CameraDepthAttachment两份RT资源作为渲染目标,而开启URP相机的CopyDepth和CopyColor设置时会额外产生_CameraDepthTexture和_CameraOpaqueTexture作为中间RT。当资源列表中出现这两种RT时,需要排查确实是否用到CopyDepth和CopyColor,否则应予以关闭以避免不必要的浪费。
9. Shader资源
9.1 ShaderLab
Unity 2019.4.20是Shader内存统计方法的一个转折点。在此之前,Shader的内存主要统计在ShaderLab中,而之后则主要统计在Shader资源自身身上。
对于Unity 2019.4.20之前的版本的项目,查看ShaderLab的内存需要在Unity Profiler中TakeSample。无论是Shader资源本体还是ShaderLab内存占用过高,都要着手于控制Shader的数量和变体数量。
9.2 变体数
变体数过多是造成一个Shader资源内存占用过大、占用包体过大的主要原因。在项目迭代中可能会出现已经被弃用或者没有被实际使用到的关键字,导致变体成倍上升;又或者Shader写的比较复杂,其中一些关键字组合永远不会被用到,从而导致很多变体是多余的。UWA的本地资源检测中提供了Shader检测功能,可以看到变体数量,定位变体数过多的Shader资源。
针对上述情况,Unity提供了回调函数,在项目打AssetBundle包或者Build时线剔除用不到的关键字或关键字组合相关的变体。剔除Shader变体的方法可以参考《Stripping scriptable shader variants》。
9.3 冗余
Shader冗余尤其需要予以关注,Shader的冗余不光导致内存上升,还可能造成重复解析,即运行时不必要的Shader.Parse和Shader.CreateGPUProgram API调用耗时。
9.4 Standard Shader
在资源列表中发现Standard、ParticleSystem/Standard Unlit。这两种Shader变体数量多,其加载耗时会非常高,内存占用也偏大,不建议直接在项目中使用。出现的原因一般是导入的FBX模型中或者Unity自身生成的一些3D对象使用了自带的Default Material,从而依赖了Standard Shader,建议予以排查精简。也可以结合UWA在线AssetBundle检测工具排查是哪个AssetBundle包中哪些资源引用了Standard Shader和ParticleSystem/Standard Unlit。如果确实要使用Standard Shader或ParticleSystem/Standard Unlit,应考虑自己重写一个Shader并只包含自己需要用到的变体。
9.5 使用本地资源检测排查Shader问题
在本地资源检测中包含了“项目中:全局关键字过多的Shader”、“项目中:可能生成变体数过多的Shader”、“Build后:生成变体数过多的Shader”、“使用了Standard Shader的材质”等上文已经提及的检测规则,方便开发者精确定位存在潜在性能问题的Shader资源。
10. 字体资源
若单个字体资源内存占用超过10MB,可以认为该字体资源内存偏大。可以考虑使用FontPruner 字体精简工具或其他字体精简工具,对字体进行瘦身,减小内存占用。
我们也需要关注项目中字体数量过多的情况,因为每个Font会对应一个Font Texture字体纹理,所以字体资源数量多了,Font Texture的数量也多了,从而占用较多内存。
11. 粒子系统
将资源列表结合粒子系统曲线来看,很多项目的内存中粒子的数量会远远高于实际Playing的粒子数量。
此时一方面需要检查是否是在迭代过程中有被弃用但未删除的粒子资源或制作过程中测试过的组件但未解除依赖;另一方面则可以考虑优化对粒子的缓存策略,减少不必要的粒子缓存。
12. Mono堆内存
UWA GOT Online Mono模式报告提供了堆内存具体分配和堆内存泄露分析两个主要功能,供开发者分析项目中堆内存存在的问题。
12.1 持续/峰值分配堆栈
在堆内存具体分配页面中,可以排查高堆内存分配函数的具体堆栈。我们主要关注两种形式的堆内存分配。
一种是单次过高的堆内存分配。这种峰值一般出现在游戏初期的读表操作导致的大量分配,需要开发者结合具体堆栈信息排查是否合理。而游戏运行过程如果还出现堆内存分配峰值则需要着重关注。
另一种则是持续偏高的堆内存分配。如果项目中存在每帧或者每隔几帧就分配较多堆内存的现象需要引起注意。持续的高堆内存分配会导致GC频率增高,从而在游戏中形成频繁的卡顿,可以结合堆栈排查是什么子节点在持续分配堆内存。
12.2 泄露分析
在泄露分析页面中排查项目中各个函数的堆内存驻留情况。选中图表中前后两处采样帧进行比较,就可以从堆栈中查看堆内存驻留情况的变化,查看驻留上升主要是什么堆栈分配造成的。
一方面可以避免堆内存持续上升造成泄露的风险,另一方面针对驻留高的函数进行优化,予以及时释放,可以降低单次GC的耗时。我们一般推荐测试GOT Online Mono模式的测试时长尽量长一些,比如1个小时,否则泄露问题往往难以被暴露。
13. 其他内存
13.1 Lua
UWA GOT Online Lua模式提供了针对Lua脚本语言的性能测试。
其中出现的函数名称格式为:函数名称@文件名:行号。
可以通过报告提供的Lua文件名/行号/函数名来定位CPU耗时的瓶颈函数和CPU耗时峰值的具体原因。Lua函数的命名格式为X@Y:Z,其中X是其函数名,在无法获取时,X会变为默认的unknown;Y是该函数定义的文件位置;Z则是该函数被定义的行号。需要注意的是,当Lua脚本以字节码运行时,该值将始终为0,因此建议在测试时尽可能使用Lua源码来运行。
针对Lua分配的内存,报告中的折线图选取了30帧内的数据最大值作为数据点。根据折线图走势,帮助开发者对项目运行过程中的堆内存分配情况有大致的了解。其中,堆内存的下降意味着发生了一次GC。查看内存具体分配和泄露分析和功能和Mono模式报告大同小异。
Lua模式报告中还有一个重要功能,即Mono对象引用统计。
从原理层面上,Unity Mono虚拟机中维护了一个对象池,用于链接Unity Object对象和Lua对象。当场景中的Unity Object对象被Destroyed之后,场景中没有了,但是由于Lua层还持有Usedata引用,导致对象池无法释放该Unity Object,如果该对象引用了Texture、Mesh等相关资源,会造成泄露。这时需要将Lua层的相关对象置空(nil),解除引用后,在下一次GC发生后,就可以回收该Unity Object对象。该功能的意义就在于辅助开发者排查此类泄露风险。
报告提供了Mono对象引用柱状图,其中黑色部分表示未被Destroyed的对象数目,由于受到Lua端GC的影响,导致会有一些Destroyed对象。这时候就要注意它是否是趋于稳定的,如果持续上涨就需要引起重视。
在柱状体选择对应帧后,列表中会显示该帧的Mono对象类型列表。其中:
对象类型:表示Unity Object对象的具体类型;
对象个数:表示这种类型的对象个数;
Destroyed对象个数:表示已经被Destroyed,但Lua层还有相关引用的这种类型的对象个数。需要关注Destroyed对象个数,如果数目较大,C#堆内存存在泄露风险。
13.2 插件和第三方库
Wwise等插件和第三方库的使用相当普遍,但一般无法在运行时定量直观地统计。不过一般它们占用的内存不大,只有在上文这些内存优化点都排查完毕后仍然发现PSS内存和Reserved Total的值之间有加大差距时,再结合插件或第三方库的文档或其开发者提供的方法进行针对性优化,甚至考虑采取性能更优的替代方案。
<hr>
本文内容就介绍到这里啦,更多内容可以前往UWA学堂进行阅读。课程将从内存、CPU、GPU三个维度讨论当前游戏项目中经常出现的一些性能问题。
页:
[1]