可以先了解一下

unity,mono和.net 的关系

1.UnityGC 和 C# GC的关系

首先,GC是C#诞生时带来的一种相较于C++这种语言的一种特性,C++需要程序员在代码编写过程中把控内存的回收。而GC则可以把程序员的这部分脑力解放出来,也就是说GC机制可以自动帮程序员完成内存的回收操作。UnityGC就是Untiy对于C#GC的一种利用和轻微的改造。UnityGC的原始逻辑模板很可能来自于mono。

2.UnityGC的硬伤

UnityGC中有一个内存池的概念。

内存池在游戏运行时只会扩张不会把内存换给系统(这句话需要更正实验结果见 5.)(UnityGC没有做到内存压缩很可能是主要原因)(该规则限定在用C#编写的逻辑中),当池子不够大的时候就会扩张池子获取内存空间。

因为没有内存压缩所以在游戏的运行过程中频繁的销毁和创建新的内存,有概率会产生非常多的内存碎片,导致内存池内部的内存连续性非常的糟糕,从而导致申请一段不长的连续内存也会让内存池的扩张。

因为疑惑我刻意对这个概念的准确性做了验证
1.打一个普通的windows mono包,进行如下操作

包占用内存上升知乎数值几乎没有变过

2.打一个windows il2cpp包 结果同1

3.用int 替换class test{} 结果同1

4.观察原神内存变化情况---游戏开始后我不停的切换场景进入副本游戏内存基本处于稳步上升状态,过程中会有100mb之内或者左右的波动(猜测:这部分很有可能是lua脚本带来的)。

5.在我按下第八或者第七次的时候unity归还了部分内存--注意是部分内存,我这几乎是一个空包,可以看到现在任然占用了2g多的内存我继续按回收键也没在下降了,所以可以理解到 unity为程序员提供了一个保底的措施,但是可以看到硬伤依然存在。

3.对内存的使用注意事项

这里我们不讨论GC是怎么实现的,有兴趣的可以看这里

https://www.drflower.top/posts/e782be43/

下面主要讨论,在GC机制下,我们制作游戏的时候应该怎么做来获得更好的游戏性能表现

1.对象池

知道了unityGC的硬伤后对象池的重要性不言而喻。这也是为什么我要把unityGC的硬伤放在前面说的原因

2.注意字符串string的使用

包括debug的使用

3.缓存

如果在代码中反复调用某些造成堆内存分配的函数但是其返回结果并没有使用,这就会造成不必要的内存垃圾,我们可以缓存这些变量来重复利用,这就是缓存。

不要在频繁调用的函数中反复进行堆内存分配

4.清除链表

在堆内存上进行链表的分配的时候,如果该链表需要多次反复的分配,我们可以采用链表的clear函数来清空链表从而替代反复多次的创建分配链表。

5.Unity函数调用

在代码编程中,当我们调用不是我们自己编写的代码,无论是Unity自带的还是插件中的,我们都可能会产生内存垃圾。Unity的某些函数调用会产生内存垃圾,我们在使用的时候需要注意它的使用。

这儿没有明确的列表指出哪些函数需要注意,每个函数在不同的情况下有不同的使用,所以最好仔细地分析游戏,定位内存垃圾的产生原因以及如何解决问题。有时候缓存是一种有效的办法,有时候尽量降低函数的调用频率是一种办法,有时候用其他函数来重构代码是一种办法。

6.装箱操作

装箱操作是指一个值类型变量被用作引用类型变量时候的内部变换过程,如果我们向带有对象类型参数的函数传入值类型,这就会触发装箱操作。比如String.Format()函数需要传入字符串和对象类型参数,如果传入字符串和int类型数据,就会触发装箱操作。

  在Unity的装箱操作中,对于值类型会在堆内存上分配一个System.Object类型的引用来封装该值类型变量,其对应的缓存就会产生内存垃圾。装箱操作是非常普遍的一种产生内存垃圾的行为,即使代码中没有直接的对变量进行装箱操作,在插件或者其他的函数中也有可能会产生。最好的解决办法是尽可能的避免或者移除造成装箱操作的代码。

7.协程

调用 StartCoroutine()会产生少量的内存垃圾,因为unity会生成实体来管理协程。所以在游戏的关键时刻应该限制该函数的调用。基于此,任何在游戏关键时刻调用的协程都需要特别的注意,特别是包含延迟回调的协程。

如果游戏中的协程产生了内存垃圾,我们可以考虑用其他的方式来替代协程。重构代码对于游戏而言十分复杂,但是对于协程而言我们也可以注意一些常见的操作,比如如果用协程来管理时间,最好在update函数中保持对时间的记录。如果用协程来控制游戏中事件的发生顺序,最好对于不同事件之间有一定的信息通信的方式。对于协程而言没有适合各种情况的方法,只有根据具体的代码来选择最好的解决办法。

8.函数引用

函数的引用,无论是指向匿名函数还是显式函数,在unity中都是引用类型变量,这都会在堆内存上进行分配。匿名函数的调用完成后都会增加内存的使用和堆内存的分配。具体函数的引用和终止都取决于操作平台和编译器设置,但是如果想减少GC最好减少函数的引用。

9.LINQ和常量表达式

由于LINQ和常量表达式以装箱的方式实现,所以在使用的时候最好进行性能测试。

参考


记录历程,整理思路,共享知识,分享思维。