最近看了B站Uinty官方有关性能优化技巧的视频,自己做一些整理。
视频链接:
堆栈(Stack)和堆积(Heap)
我们先来看下Unity内存中重要的两部分,堆栈和堆积,因为只有了解了它们,我们才能知道应该如何优化内存,提高性能。
堆栈:
堆栈是内存中存储函数和值类型的地方。
例如我们调用一个函数A,会将这个函数体与函数收到的参数放入到堆栈中,若在函数A中调用函数B,同样会把函数B存放到堆栈中。当函数B运行结束,会将其从堆栈中移除,然后当A运行结束,把A从堆栈中移除。
因此我们在看Debug信息的时候,就会发现Log里面能够做到一层层的方法回溯,方便我们查看整体的调用过程,这也就是堆栈回溯。
由于是堆栈的结构,因此不会遇到碎片化或是垃圾收集(GC)的问题。但是可能会碰见堆栈溢出的问题,比如调用了太多的函数导致一直push东西进堆栈,占据越来越多的内存空间,导致堆栈溢出。
堆积:
堆积是内存中另一个区域,要比堆栈大,我们将所有的引用类型存放在这。通常我们每创建一个新的对象,会在堆积中找到下一个足够存放的空位置,将其存储。但是当我们销毁对象后,内存空间不会马上释放出来,而是标记成未使用,之后垃圾收集器会释放这部分空间。
对象实例化和摧毁的过程其实很慢,所以我们要尽可能地避免在堆积中配置内存的行为。如果我们需要的内存比之前已经配置好的还多,在放不下的情况下,堆积会膨胀,并且每次都增长两倍,且不会再缩回去,过大的堆积就会影响到我们游戏的性能。当我们在堆积中释放了一些占用空间小的对象,而后添加一些占用空间大的对象时,由于前面释放的空间不足以存放下,就会导致这些空间空出来,使得内存的使用情况就变得断断续续起来,这也就是内存的碎片化,同样降低我们的游戏性能。
垃圾收集(GC)的原理:每一次GC,都会遍历堆积上所有的对象,找到需要释放的东西,然后将其释放。
假如游戏玩到一半,GC必须要释放数十或数百个游戏对象的内存,那么这会对你的游戏过程造成一个负载峰值,我们要避免这样的负载峰值。
编程过程中的一些优化建议
1.选择合适的数据结构
数据结构,也就是Array,List和Dictionary等,例如在Array或List中使用索引的成本很低,那么就适合要经常通过索引读取的情况。而要频繁增加和移除对象时,使用Dictionary是最合适的。
2.对象池
在游戏程序中,创建和销毁对象事很常见的操作,通常会通过 Instantiate 和 Destroy 方法来实现,如果频繁的进行这些操作,GC的时候会导致负载很重,因为会有大量的已摧毁对象的存在,不仅会造成CPU的负载峰值,还可能导致堆积碎片化。因此我们可以使用对象池来处理这类问题。
使用对象池时需要注意,要决定对象池的大小,以及一开始要产生多少数量的对象在池中。因为如果你需要的对象数量多过池中现有的,就必须将对象池变大,扩的太大可能造成浪费,扩的小可能又造成频繁的添加。
3.Scriptable Objects
假设我们有一个控制敌人的组件,名叫Enemy,代码如下:
public class Enemy : MonoBehaviour
{
public float maxSpeed;
public float attackRadius;
}
这个组件挂载在每个敌人身上,但是其中这两个浮点数(maxSpeed 和 attachRadius)的数值都是不变的。那么当场景中存在很多的敌人时,每次生成敌人的时候,这些数据就会重复一份。
所以即使所有数据都一样,这两个浮点数还是重复的出现在有此脚本的对象上。所以建议改用Scriptable Objects,这样就只会耗费一组这样数据的内存,代码如下:
public class EnemyConfiguratio