Unity3D 进阶教程 - 12 内存优化与 GC 调优

概述

Unity 的内存分为 原生内存(纹理、网格、着色器)和 托管内存(C# 对象和 MonoBehaviour)。其中 GC(垃圾回收) 是性能抖动的主要来源。

入门系列第 13 篇协程中提到了 foreach 的装箱问题。本篇全面覆盖 Unity 内存系统的底层原理、GC 产生的原因以及调优策略、常见的内存泄漏场景。

1. Unity 内存架构

1.1 三大区域

1
2
3
托管堆(Mono/IL2CPP)   <- C# 对象、List、string
原生内存 <- 纹理、网格、音频、动画
执行代码 <- DLL、IL2CPP 编译的代码
区域 分配方式 释放方式 碎片问题
托管堆 new、+= 字符串 GC 自动回收 严重
原生内存 Unity 自动分配 Destroy / Resources.Unload 较少
执行代码 加载时分配 不可释放

1.2 IL2CPP vs Mono

特性 Mono IL2CPP
GC 实现 Boehm GC(保守式) 分代 GC(精确式)
性能 解释执行 AOT 编译,快 3-5 倍
包体 较小
64 位 支持 原生支持

建议:正式发布使用 IL2CPP,编辑器开发阶段用 Mono。


2. GC 的触发时机

GC 在以下情况触发:

  1. 托管堆达到阈值时自动触发
  2. 手动调用 System.GC.Collect()
  3. 场景切换时(部分平台)
  4. iOS 内存警告时

2.1 常见 GC 分配来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 问题 1:字符串拼接
_scoreText.text = "Score: " + _score; // 每帧分配新字符串

// 修复:使用 StringBuilder
private readonly StringBuilder _sb = new();
_sb.Clear();
_sb.Append("Score: ");
_sb.Append(_score);
_scoreText.text = _sb.ToString();

// 问题 2:LINQ
var activeEnemies = enemies.Where(e => e.isActiveAndEnabled).ToList();

// 修复:用 for 循环
var activeEnemies = new List<Enemy>();
for (int i = 0; i < enemies.Count; i++)
{
if (enemies[i].isActiveAndEnabled)
activeEnemies.Add(enemies[i]);
}

// 问题 3:装箱
string msg = string.Format("Damage: {0}", damage); // int -> object 装箱

2.2 GC 分配表

操作 是否产生 GC 替代方案
string + string StringBuilder
foreach 遍历非数组 for 循环
LINQ.Where/.Select 手写循环
int -> object 装箱 使用泛型
yield return 协程 UniTask
值类型 struct -

3. 内存优化策略

3.1 Object Pool 减少 GC

1
2
3
4
5
// 错误:频繁创建和销毁
Destroy(bullet, 2f);

// 正确:对象池复用
pool.Return(bullet);

3.2 使用 ArrayPool

1
2
3
4
5
6
7
8
9
10
11
using System.Buffers;

var nodes = ArrayPool<PathNode>.Shared.Rent(1000);
try
{
// 处理路径...
}
finally
{
ArrayPool<PathNode>.Shared.Return(nodes);
}

3.3 缓存与重用

1
2
3
4
5
6
7
8
9
10
11
12
// 不用每帧 GetComponent
private ParticleSystem _cache;

private void Awake()
{
_cache = GetComponent<ParticleSystem>();
}

private void Update()
{
_cache.Play();
}

4. 内存泄漏排查

模式 示例 解决
静态集合不当 static List 及时移除
委托未解除 OnEnable 订阅,OnDisable 未取消 配对管理
Resources 未释放 Resources.Load 未 Unload 手动释放
场景引用 DontDestroyOnLoad 持有场景引用 在 OnDestroy 置空

5. 减少 GC 的编码规范

1
2
3
4
5
6
7
8
9
10
11
12
13
允许:
for 循环
StringBuilder
值类型 struct
对象池
缓存 GetComponent 结果
UniTask

避免:
每帧 string 拼接
LINQ 在 Update 中使用
foreach 遍历非数组
频繁 Instantiate / Destroy

总结

  • GC 是卡顿的主要原因之一,目标是减少 GC 分配量
  • 字符串操作、LINQ、装箱、频繁 Instantiate 是最大 GC 来源
  • 对象池、ArrayPool、StringBuilder、struct 是主要优化手段

如果项目 GC Alloc/帧 能控制在 2KB 以下,性能抖动几乎不可感知。

下一篇将讲解 代码性能优化,从数据结构选择和算法层面进一步提升运行时效率。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: