游戏测试 C# 代码优化:斩断伸向堆内存的 “黑手”

侑虎科技 · 2021年04月07日 · 1906 次阅读

在上期《C# 代码优化:拯救你的 CPU 耗时》中,我们依托UWA 本地资源检测,从 “时间” 的角度对 C# 代码检测中和 CPU 耗时相关的知识点为大家进行了简单的讲解。本篇将从 “空间” 角度入手,为大家继续梳理 C# 代码检测的相关规则。

C# 是在虚拟机(VM)环境当中运行的(Mono 虚拟机或 IL2CPP 虚拟机),其分配的堆内存是由虚拟机进行管理与回收的,因此,C# 代码被称为 “托管代码”,其堆内存又被称为 “托管内存”。托管内存的释放依赖于虚拟机的 GC(垃圾回收)机制,本文就不展开讨论了。

C# 程序开发要遵循的一个基本原则就是避免不必要的堆内存分配,而堆内存分配主要会造成以下后果:

  • 程序所占用的内存总量过大。内存是有限的资源,对于游戏(特别是移动游戏)来说,内存的占用可谓是寸土寸金。过多的无法释放的内存很可能导致程序崩溃的现象。
  • 过多的分配次数会导致堆碎片变多,从而可能导致无法开辟出所需要的连续的内存,这也会导致程序崩溃。
  • 内存分配会触发 GC(垃圾回收),而 GC 的代价较高,会造成卡顿。


1、该类的方法中存在 .tag 的调用

tag 是场景中 GameObject 的标签,而类 GameObject 的成员 tag 是一个属性,在获取该属性时,实质上是调用 get_tag() 函数,从 native 层返回一个字符串。字符串属于引用类型,这个字符串的返回,会造成堆内存的分配。然而,Unity 引擎也没有通过缓存的方式对 get_tag 进行优化,在每次调用 get_tag 时,都会重新分配堆内存。

所以当需要对 tag 进行比较时,我们建议使用函数 GameObject.CompareTag(),该函数是在 native 层实现的,不会造成托管堆内存的分配,也就避免了 GC 压力。


2、该类的方法中存在对纹理 GetPixels()/GetPixels32() 调用

对 Texture2D 类型的对象调用 GetPixels() 和 GetPixels32(),一般都是为了获取指定 Mipmap 层的全部像素信息,而图片上的像素数量往往是很庞大的。

从内存分配上讲,该函数会在托管堆中分配内存,用以存储纹理数据的像素信息,但引擎不会对其进行缓存。所以如果在频繁调用的函数中使用,就会造成持续性的堆内存分配。

从耗时上讲,擅长执行大规模并行运算的 GPU 来处理图片信息是非常容易的,但 CPU 在进行逐个像素信息的获取时,就显得有些吃力了。并且 GetPixels() 在实现上是由 CPU 同步执行的,所以耗时会较高,同时会阻塞调用的线程,从而可能会造成卡顿。因此在非必要的情况下,并不建议使用 GetPixels()。


3、该类的方法中存在 GetComponentsInChildren 调用/该类的方法中存在 GetComponentsInParent 调用

在之前的文章《C# 代码优化:拯救你的 CPU 耗时》中,我们对 GetComponentsInChildren 和 GetComponentsInParent 进行了简单地讲解。在这里,我们进一步补充说明,这两者在实际调用中,因为涉及到对象的遍历和结果的返回,所以如果使用不当,就会造成持续性的堆内存分配。我们建议开发团队使用接受 List 类型的引用作为参数的版本,这样就可以避免每次调用都造成堆内存的分配。


4、该类的方法中存在 Linq 相关函数的调用

Linq 相关的函数一般都用于对数据的查询和处理。功能上简单来讲,就是对一堆数据进行各种 if 判断和 for 循环处理。使用 Linq 提供的 API,我们可以写出 SQL 语句风格的代码来进行集合数据的处理,这能够明显提升我们代码的简明性、可读性,维护上也更方便,从而提升编写效率,但是这些优点是以性能的开销为代价的。

Linq 在执行过程中会产生一些临时变量,而且会用到委托(lambda 表达式)。如果使用委托作为条件的判定方法,时间开销就会很高,并且会造成一定的堆内存分配。所以在一般的 Unity 游戏项目开发中,我们不推荐使用 Linq 相关的函数。在编辑器功能开发中,我们才常常把 Linq 和 Reflection 进行配合使用。


5、该类的方法中存在对 Renderer 进行 sharedMaterials 的获取

同样,在之前的文章《C# 代码优化:拯救你的 CPU 耗时》中,我们对.material/materials 进行了讲解。简单地说,对.material(s) 的调用会产生新的材质球实例;而 sharedMaterials 则是共享材质,不会生成新的材质实例。然而,对.sharedMaterials 的调用,依旧会分配堆内存——每次调用都会分配一个用以存放 Material 的索引的数组。虽然该数组占用的内存相对较小,但我们还是建议不要对其进行频繁地调用。


6、该类的方法中存在 Input.touches 调用

移动端项目交互里,点触操作可以说是极为频繁与常见。在点触操作的获取上,Input.touches 就是用来获取当前帧中所有点触操作的状态和相应数据。但是查看.touches 的实现,我们就会发现:每次在对其调用时,都会 new 一个数组 touches,从而造成一定的堆内存分配。所以开发团队要避免 Input.touches 的频繁使用以防造成堆内存的额外占用。


7、FindObjectsOfType 调用

在之前的文章《C# 代码优化:拯救你的 CPU 耗时》中,我们对 FindObjectsOfType 进行了简单地介绍。它在增大 CPU 耗时的同时,也会占用相当一部分的堆内存分配,所以建议通过一次调用,缓存结果的方式减少其带来的性能影响。


8、该类的方法中存在 TextAsset/WWW.bytes 调用

该规则针对的其实是两条不相关的 Unity API。首先是 TextAsset 的 bytes 属性。TextAsset 是 Unity 中的一种文本资源,它支持包括 txt、html、bytes 和 csv 等多种格式的文件进行转换。在获取 bytes 属性时,Unity 会从 native 层获取字节数组(byte[]),从而分配一定的堆内存。

另一个 API 指的是 WWW 这个类的 bytes 成员,每次对其进行调用都会导致堆内存的分配,需要指出的是 Unity 已经放弃了 WWW 这个类的相关接口,所以我们建议大家使用 UnityWebRequest 来实现相关的功能。

===

以上我们就目前本地资源检测中提供的检测项均做了分析和解读,希望以上这些知识点能在实际的开发过程中为大家带来帮助,同时我们也会基于广大开发者的需求反馈,不断增加新的检测项。

需要说明的是,每一项检测规则的阈值都可以由开发团队依据自身项目的实际需求去设置合适的阈值范围,这也是本地资源检测的一大特点。同时,也欢迎大家来使用 UWA 推出的本地资源检测服务,可帮助大家尽早对项目建立科学的美术规范。




万行代码屹立不倒,全靠基础掌握得好!

《场景检测:面片、光影和物理属性》
《场景检测:Audio Listener、RigidBody 和 Prefab 连接》
《场景检测:雾效、Canvas 和碰撞体》
《特效优化 2:效果与性能的博弈》
《特效优化:发现绚丽背后的质朴》
《Prefab 优化:预制体中的各种细节选择》


性能黑榜相关阅读

《那些年给性能埋过的坑,你跳了吗?》
《那些年给性能埋过的坑,你跳了吗?(第二弹)》
《掌握了这些规则,你已经战胜了 80% 的对手!》

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册