[Unity优化]内存管理与程序性能优化

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jingangxin666/article/details/82564238

1. Unity 的自动内存管理

  • Unity 内部有两个内存管理池: 堆内存和堆栈内存. 堆栈内存 (stack) 主要用来存储较小的和短暂的数据, 堆内存 (heap) 主要用来存储较大的和存储时间较长的数据.
  • Unity 中的变量只会在堆栈或者堆内存上进行内存分配, 变量要么存储在堆栈内存上, 要么处于堆内存上
  • 只要变量处于激活状态, 则其占用的内存会被标记为使用状态, 则该部分的内存处于被分配的状态.
  • 一旦变量不再激活, 则其所占用的内存不再需要, 该部分内存可以被回收到内存池中被再次使用, 这样的操作就是内存回收. 处于堆栈上的内存回收及其快速, 处于堆上的内存并不是及时回收的, 此时其对应的内存依然会被标记为使用状态.
  • 垃圾回收主要是指堆上的内存分配和回收, Unity 中会定时对堆内存进行 GC 操作.

2. 堆栈内存分配和回收机制

堆栈上的内存分配和回收十分快捷简单, 因为堆栈上只会存储短暂的或者较小的变量. 内存分配和回收都会以一种顺序和大小可控制的形式进行.

堆栈的运行方式就像 stack: 其本质只是一个数据的集合, 数据的进出都以一种固定的方式运行. 正是这种简洁性和固定性使得堆栈的操作十分快捷. 当数据被存储在堆栈上的时候, 只需要简单地在其后进行扩展. 当数据失效的时候, 只需要将其从堆栈上移除.

3. 堆内存分配和回收机制

堆内存上的内存分配和存储相对而言更加复杂, 主要是堆内存上可以存储短期较小的数据, 也可以存储各种类型和大小的数据. 其上的内存分配和回收顺序并不可控, 可能会要求分配不同大小的内存单元来存储数据.

3.1 堆上的变量在存储时的步骤

  1. 首先, Unity 检测是否有足够的闲置内存单元用来存储数据, 如果有, 则分配对应大小的内存单元
  2. 如果没有足够的存储单元, Unity 会触发垃圾回收来释放不再被使用的堆内存. 这步操作是一步缓慢的操作, 如果垃圾回收后有足够大小的内存单元, 则进行内存分配.
  3. 如果垃圾回收后并没有足够的内存单元, 则 Unity 会扩展堆内存的大小, 这步操作会很缓慢, 然后分配对应大小的内存单元给变量.
  4. 堆内存的分配有可能会变得十分缓慢, 特别是在需要垃圾回收和堆内存需要扩展的情况下, 通常需要减少这样的操作次数.

3.2 垃圾回收时的操作

当堆内存上一个变量不再处于激活状态的时候, 其所占用的内存并不会立刻被回收, 不再使用的内存只会在 GC 的时候才会被回收

每次运行 GC 的时候, 主要进行下面的操作:

  1. GC 会检查堆内存上的每个存储变量
  2. 对每个变量会检测其引用是否处于激活状态
  3. 如果变量的引用不再处于激活状态, 则会被标记为可回收
  4. 被标记的变量会被移除, 其所占有的内存会被回收到堆内存上

GC 操作是一个极其耗费的操作, 堆内存上的变量或者引用越多则其运行的操作会更多, 耗费的时间越长.

3.3 何时会触发GC

  • 在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存
  • GC 会自动的触发, 不同平台运行频率不一样
  • GC 可以被强制执行.

特别是在堆内存上进行内存分配时内存单元不足够的时候, GC会被频繁触发, 这就意味着频繁在堆内存上进行内存分配和回收会触发频繁的 GC 操作

3.4 GC引起的性能问题

主要表现为:

  • 帧率下降 —— 在性能瓶颈时期触发GC
  • 性能时好时坏
  • 断断续续的出现卡顿 —— 每次GC的到来, 会导致系统停止所有进程, 并造成大量的CPU 开销, 进而降低游戏运行的流畅度.

如何降低降低GC带来的影响:

  • 减少GC运行所需的时间 —— 减少游戏中的堆内存分配和对象引用数目
  • 减少GC频率 —— 降低对内存的分配和释放频率
  • 在非性能瓶颈时期主动触发GC —— 尝试测算GC和堆空间扩张的时间来使其在可预测, 适宜的时间发生

3.5 如何减少垃圾的产生

  • 使用缓存&不要在频繁调用的方法中分配堆内存. 如果在需要频繁调用的方法中进行了堆内存分配, 并且最后舍弃了这个新建的变量, 这将会产生不必要的垃圾, 应该考虑在方法外创建对该变量的引用, 然后重复利用它. 对于无法缓存的对象, 应该尽量降低方法的执行次数, 仅在需要时才去执行它.
  • 使用清空集合替代新建集合. 创建新的集合对象将会在堆空间分配内存. 如果代码中多次创建新的集合, 应该缓存对集合的引用, 在下次需要使用新的集合时, 将缓存的集合清空 ( Clear() ) 而不是建立全新的集合.
  • 使用对象池. 如果游戏中需要频繁的创建再销毁某类对象, 那么为该类对象建立对象池.

4. C#内存与性能优化

C#是一门非常方便的语言可以帮助我们快速开发. 不过有一些影响内存与性能的要点需要关注

4.1 foreach和GetEnumerator的使用

产生原因:

  • Array中的Enumerator是对象类型, 这是intArray调用GetEnumerator产生GCAlloc的原因.
  • 泛型List中的Enumerator是值类型, 所以它不会产生GCAlloc. 而foreach应用于List时, 由于增加了一个box装箱操作, 所以产生了GCAlloc.

解决方法:

  1. 如果能使用数组, 就直接使用数组, 对它直接使用foreach不产生GC Alloc.
  2. 尽可能不要使用数组的GetEnumerator 方法, 会产生新GC Alloc.
  3. 当我们需要动态数组时, 最好使用List这种泛型格式. 当遍历它们时, 我们不要使用foreach, 而应该改用GetEnumerator.
  4. 尽可能避免使用ArrayList, 对它的遍历操作均会产生新的GC Alloc.
  5. 不同的Unity版本上, 发现泛型List无论使用foreach还是GetEnumerator都会产生GC, 于是另外一篇文章《【Unity优化】构建一个拒绝GC的List》, 文章中提供了作者自己构建的List, 虽然没有系统的List功能全面, 不过常用的情况都足以应对, 而且效率应该是更高效的

参考文章:

4.2 Coroutine造成的GC:

代码示例:

yield return new WaitForEndOfFrame()
yield return new WaitForFixedUpdate();
yield return new WaitForSeconds(1.0f);

产生原因:

  • 协程中的yield语句本身不需要进行堆内存分配, 但由它所返回的值可能需要分配堆内存
  • 在yield return中出现了new也就意味着可能会带来一次GC, 那么每一次的yield return 都会产生新对象
  • 调用StartCoroutine()方法会产生少量的垃圾, 因为Unity需要创建一些类的实例用于管理协程.

解决方法:

  • 在Unity中, 我们可以保存对象的方式来避免对象的分配.

参考文章:

4.3 避免频繁Instantiate

产生原因:

  • 频繁的Instantiate 会造成大量的堆内存分配, 即使通过Destroy 销毁实例化的GameObject, 内存中也会驻留大量的
    内存碎片, 从而导致堆内存的快速升高, 进而加快系统调用GC 的频率

优化方法:

  • 通过缓冲池来复用相关的GameObject

4.4 字符串String

字符串拼接

  • 在C#里面, 字符串是一个引用类型而不是一个值类型, 修改字符串是创建一个新的字符串
  • 通常建议使用StringBuilder来拼接字符串
  • 我们在平时被建议使用Format来拼接字符串
    • 实际Format的内部使用了StringBuilder来拼接字符串
    • 但在有些情况下Format的表现非常差(装箱操作)

字符串数量

  • 每次创建字符串都会得到一个新字符串, 即使已经存在一个相同的字符串
  • 可以通过string.Intern来减少字符串数量达到优化内存的效果
  • 项目中字符串路径数据长, 数量多, 作用是标识资源
    • 使用Hash来标识资源也可以做到相同的事情
    • 计算路径的Hash还需要考虑路径的大小写, 斜杠反斜杠
    • 使用ulong降低Hash的冲突
    • 在日常构建的时候对所有的资源路径计算Hash判断是否有冲突
  • Unity的Animator类提供了StringToHash接口来帮助消除字符串, 同时配套提供两套接口可以调用

字符串比较

  • 默认的字符串比较操作是非常低效的
  • 正常情况下使用Ordinal比较即可
  • 必要时可以自己实现Ordinal行为的比较

参考文章:

4.5 GameObject.SetActive

GameObject.Activate/Deactivate耗时较大, 也会造成GC

产生原因:

  • 实际上GameObject.Activate/Deactivate本身通常不会产生很高的开销
  • 主要都是由其上或其子节点上的组件的OnEnable/OnDisable操作引起
    • 比如UI相关的组件在OnEnable中会有较多的初始化操作
      • UILabel.OnEnable操作, 主要是初始化文本网格的信息(每个文字所在的网格顶点, UV, 顶点色等等属性), 而这些信息都是储存在数组中(即堆内存中)
      • 所以文本越多, 堆内存开销越大. 但这是不可避免的, 只能尽量减少出现次数

优化方法:

  • 切换频率最高的UI界面, 可以通过改变UI的位置( 以 UIPanel 为单位 )来实现 UI 的隐藏和显示. 因为是位置移动, 所以并不产生多余的堆内存和CPU消耗, 同时又可以节省Enable和Disable的CPU开销.
  • 将显示/隐藏最为频繁的UI 元素通过修改Scale的方式来进行隐藏, 从而避免SetActive导致的FillAllDrawCalls, 从而减少CPU消耗
  • 通过设置相机的Culling mask , 以及动态切换UIPanel的Layer来实现UI界面的隐藏和显示,同样避免Enable/Disable操作.

参考文章:

4.6 闭包 ( Closures ) 和匿名方法

使用闭包和匿名方法时考虑两点:

  • C#中的所有方法引用都是引用类型, 因此在堆上分配内存. 无论传递的方法是匿名方法还是预定义方法, 将方法引用作为参数传递, 都会造成堆内存分配
  • 将匿名方法转换为闭包, 会显著提高传递闭包所需的内存量
  • 因为执行闭包需要实例化其生成的类的副本, 并且所有类都是C#中的引用类型, 所以执行闭包需要在托管堆上分配对象

优化方法:

  • 最好尽可能避免C#中的闭包
  • 应该在性能敏感的代码中最小化匿名方法和方法引用, 尤其是在基于每帧执行的代码中
  • 在不可避免地使用闭包的情况下, 优先使用匿名方法而不是预定义方法

参考文章:

4.7 List.Add

产生原因:

  • List底层是数组, 在数组容量不够的时候就会扩充, 会产生GC

优化方法:

  • 可以考虑在new的时候直接指定大小

4.8 Equals

int x = 1;
object y = new object();
y.Equals(x);

产生原因:

  • 在这个非常简单的示例中, x中的整数被装箱以便传递给object.Equals方法,
  • 因为Equals的参数是object, 这里还是有一个装箱的过程

优化方法:

  • 重载==号, 而不是直接使用Equals来比较两个struct是否相等

4.9 Boxing

Boxing的触发条件:

  • 当需要将栈 ( Stack ) 上的值类型转换为堆 ( Heap ) 上的引用类型, 这个过程被称为“装箱”

Boxing具有以下特性:

1.在堆 ( Heap ) 上分配空间

2.通知垃圾回收器有关新对象的信息

3.复制值类型对象中的数据并传递给新的引用类型对象

参考文章:

4.10 Update内刷新UI数值会产生GC

示例代码:

//Character.cs
using UnityEngine;
public class Character : MonoBehaviour {
    public float HP = 10;
    public float MaxHP = 10;
}

//CharView.cs
using UnityEngine;
using UnityEngine.UI;
public class CharView: MonoBehaviour{
    public Text TextHP;
    public Character Char;//角色
    void Update(){
        TextHP.text = string.Format("{0}/{1}", Char.HP, Char.MaxHP);
    }
}

产生原因:

  • 浮点类型进行了装箱: public static string Format(string format,object arg0,object arg1)方法参数类型是object类型
  • 调用了ToString(): string.Format的每个参数都先调用了ToString
  • string.Format本身字符串拼接的消耗

解决方法:

  • 使用函数SetDirty和标志Dirty Flag,
  • 在某些事件发生的时候, 调用SetDirty, 例如刷新按钮上或标题上的文字, 每一天开始刷新显示日期时
  • 对于无法用事件来概括变化原因的, 可以检测变量是否变化, 来决定是否调用SetDirty

参考文章:

4.11 LINQ和正则表达式

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

5. 拓展阅读:

猜你喜欢

转载自blog.csdn.net/jingangxin666/article/details/82564238