一、GC相关知识点
1.GC的概念
- C#内部有两个内存管理池:堆内存和栈内存。栈内存(stack)主要用来存储较小的和短暂的数据,堆(heap)内存主要用来存储较大的和存储时间较长的数据。C#中的变量只会在栈或堆内存上进行内存分配,变量要么存储在栈内存上,要么在堆内存上。
- 只要变量处于激活状态,则其占用的内存会被标记为使用状态,则该部分的内存处于被分配的状态。
- 一旦变量不再激活,则其所占用的内存不再需要,该部分内存可以被回收到内存池中被再次使用,这样的操作就是内存回收。处于栈上的内存回收及其快速,处于堆上的内存并不是及时回收的,此时其对应的内存依然会被标记为使用状态,不再使用的内存只会在GC的时候才被回收。
- 垃圾回收主要是指堆上的内存分配和回收,C#中会定时对堆内存进行GC操作。
2.GC会带来的问题
- 游戏性能:GC操作是一个及其耗费时间的操作,堆内存上的变量或者引用越多则导致遍历检查时的操作变得十分缓慢,使得游戏运行缓慢,例如当CPU处于游戏性能的关键时刻,任何一个操作就会导致游戏帧率下降,造成极大影响。
- 游戏内存:(UnityGC采用的是非分代非压缩的标记清除算法)GC操作会产生“内存碎片化”。当一个单元内存从堆中分配出来,其大小取决于存储变量的大小。当内存被回收到堆上时,有可能被堆内存分割成碎片化的单元,即下次分配时找不到合适的储存单元,就会触发GC操作,或者堆内存扩容操作,导致GC频繁发生和游戏内存越来越大。
3.GC触发时机
- 在堆内存上进行内存分配操作,而内存不够的时候都会触发垃圾回收来利用闲置的内存。
- GC会自动触发,不同平台运行帧率不一样。
- GC可以被强制执行。
4.如何避免GC?
- 减少临时变量的使用,多使用公共对象,多利用缓存机制。(将容器定义到函数外,用到容器的时候进行修改即可)
- 减少new对象的次数。
- 对于大量字符串拼接时,用StringBuilder代替string(string不可修改性,修改即创建一个新的string对象,旧的直接抛弃等待GC,但少量字符串拼接用string,性能优于StringBuilder)
- 使用扩容的容器时,例如List,StringBuilder等,定义时尽量根据存储变量的内存大小定义存储空间,减少扩容的操作(扩容后旧的容器直接抛弃等待GC)
- 代码逻辑优化:例如当计时器大于1s后才进行文本修改,而不是每帧都修改,或者禁止在关键时候GC,影响游戏性能,可以在加载页面或者进度条的时候GC。
- 利用对象池:对象池是一种Unity经常用到的内存管理服务,针对经常消失生成的对象,例如子弹,怪物等,作用在于减少创建每个对象的系统开销。在我们想要对象消除时,不直接Destory,而是隐藏起来SetActive(false),放入池子中,当需要再次显示一个新的对象时,先去池子中看有没有隐藏对象,有就取出来(显示) SetActive(true),没有的话,再实例化。
- 减少装箱拆箱操作。(装箱是将值类型转换为 object 类型或由此值类型实现的任何接口类型的过程)
- 协程: yeild return 0 会产生装箱拆箱,可以替换为 yeild return null。
二、结构体和类
1.区别:
- 结构体:值类型→存在栈中→进行值传递→在函数中改变参数值时值不会变化
- 类:引用类型→存在堆中→进行引用传递(传递指针)→在函数中改变参数值时值会变化
- 结构体:定义结构体类型时成员不能初始化,定义结构体变量时所有成员都要自己赋值初始化
- 类:定义时可以初始化成员变量,定义对象时也可以重新赋值
- 结构体:不能申明无参构造,申明有参构造不会顶掉无参,有参构造需要初始化所有成员变量,不能申明析构函数,不能被继承,不能被静态static修饰
- 类:可以申明无参构造,申明有参构造会顶掉无参,有参构造不需要初始化所有成员变量,可以申明析构函数,可以被继承,可以被静态static修饰
声明了结构类型后,可以使用new运算符创建构造对象,也可以不使用new关键字。如果不使用new,那么在初始化所有字段之前,字段将保持未赋值状态且对象不可用。
2.使用情景
结构体:
- 结构体是值类型在栈中,栈的存取速度比堆快,但是容量小,适合轻量级的对象,比如点、矩形、颜色。
- 如果对象是数据集合时,优先考虑接结构体(位置,坐标)
- 在变量传值的时候,希望传递对象的是拷贝,而不是对象的引用地址,这个时候就可以使用结构体。
类:
- 类是引用类型,存储在堆中,堆的容量大,适合重量级的对象,栈的空间不大,大量的对应当存在于堆中。
- 如果对象需要继承和多态特征,用类(玩家、怪物)。