Unity 基本的内存节省方法

可使用一些相对简单的技术来减少托管堆分配。

1.集合和数组重用

使用 C# 的集合类或数组时,尽可能考虑重用或汇集已分配的集合或数组。集合类开放了一个 Clear 方法,该方法会消除集合内的值,但不会释放分配给集合的内存。

2.闭包和匿名方法

使用闭包和匿名方法时需要注意两点。

首先,C# 中的所有方法引用都是引用类型,因此在堆上进行分配。通过将方法引用作为参数传递,可以轻松分配临时内存。无论传递的方法是匿名方法还是预定义的方法,都会发生此分配。

其次,将匿名方法转换为闭包后,为了将闭包传递给接收闭包的方法,所需的内存量将显著增加。

因为执行闭包需要实例化闭包生成类的副本,并且所有类都是 C# 中的引用类型,所以执行闭包需要在托管堆上分配对象。

通常,请尽可能在 C# 中避免使用闭包。应在性能敏感的代码中尽可能减少匿名方法和方法引用,尤其是那些每帧都需要执行的代码中。

3.装箱 (Boxing)

装箱是 Unity 项目中最常见的非预期临时内存分配来源之一。只要将值类型的值用作引用类型就会发生装箱;这种情况最常发生在将原始值类型的变量(例如 int 和 float)传递给对象类型的方法时。

C# IDE(集成开发环境)和编译器通常不会发出关于装箱的警告,即使导致意外的内存分配时也是如此。这是因为 C# 语言的设计理念认为,小型临时分配可以被分代垃圾回收器和对分配大小敏感的内存池有效处理。

虽然 Unity 的分配器实际会使用不同的内存池进行小型和大型分配,但 Unity 的垃圾回收器“不是”分代的,因此无法有效清除由装箱生成的小型、频繁的临时分配。

在为 Unity 运行时编写 C# 代码时,应尽可能避免使用装箱。

4.字典和枚举

装箱的一个常见原因是使用 enum 类型作为字典的键。声明 enum 会创建一个新值类型,此类型在后台视为整数,但在编译时实施类型安全规则。

默认情况下,调用 Dictionary.add(key, value) 会导致调用 Object.getHashCode(Object)。此方法用于获取字典的键的相应哈希代码,并在所有接受键的方法中使用,如:Dictionary.tryGetValue、Dictionary.remove 等。

Object.getHashCode 方法为引用类型,但 enum 值始终为值类型。因此,对于枚举键字典,每次方法调用都会导致键被装箱至少一次。

5.Foreach 循环

在 Unity 的 Mono C# 编译器版本中,使用 foreach 循环会在每次循环终止时强制 Unity 将一个值装箱(__注意:__是在每次整个循环完整执行完成后将该值装箱一次,并非在循环的每次迭代中装箱一次,因此无论循环运行两次还是 200 次,内存使用量都保持不变)。这是因为 Unity 的 C# 编译器生成的 IL 会构造一个通用值类型的枚举器来遍历值集合。

通常,应在 Unity 中避免使用 foreach 循环。原因不仅是这些循环会进行装箱,而且通过枚举器遍历集合的方法调用成本更高,通常比通过 for 或 while 循环进行的手动迭代慢得多。

请注意,Unity 5.5 中的 C# 编译器升级版本显著提高了 Unity 生成 IL 的能力。特别值得注意的是,已从 foreach 循环中消除装箱操作。因此,节约了与 foreach 循环相关的内存开销。但是,由于方法调用开销,与基于数组的等效代码相比,CPU 性能差距仍然存在。

6.Unity 数组值 API

虚数组分配的一种更有害和更不明显的原因是重复访问返回数组的 Unity API。返回数组的所有 Unity API 每次被访问时都会创建一个新的数组副本。在不必要的情况下访问数组值 Unity API 是极不适宜的。

例如,下面的代码在每次循环迭代时都会虚化创建 vertices 数组的四个副本。每次访问 .vertices 属性时都会发生分配。

for(int i = 0; i < mesh.vertices.Length; i++)

{

    float x, y, z;

    x = mesh.vertices[i].x;

    y = mesh.vertices[i].y;

    z = mesh.vertices[i].z;

    // ...

    DoSomething(x, y, z);   

}

通过在进入循环之前捕获 vertices 数组,无论循环迭代次数是多少,都可以简单地重构为单个数组分配:

var vertices = mesh.vertices;

for(int i = 0; i < vertices.Length; i++)

{

    float x, y, z;

    x = vertices[i].x;

    y = vertices[i].y;

    z = vertices[i].z;

    // ...

    DoSomething(x, y, z);   

}

虽然访问一次属性的 CPU 成本不是很高,但在紧凑循环内重复访问会使得 CPU 性能过热。此外,重复访问会导致托管堆出现不必要的扩展。

此问题在移动端极其常见,因为 Input.touches API 的行为与上述类似。项目包含以下类似代码是极为常见的,此情况下每次访问 .touches 属性时都会发生分配。

for ( int i = 0; i < Input.touches.Length; i++ )

{

   Touch touch = Input.touches[i];

    // …

}

7.空数组重用

当数组值方法需要返回空集时,有些开发团队更喜欢返回空数组而不是 null。这种编码模式在许多托管语言中很常见,特别是 C# 和 Java。

通常情况下,从方法返回零长度数组时,返回零长度数组的预分配单例实例比重复创建空数组要高效得多(5)(__注意:__当然,在返回数组后调整数组大小时是个例外)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值