概述 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 在以下情况触发:
托管堆达到阈值时自动触发
手动调用 System.GC.Collect()
场景切换时(部分平台)
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 _scoreText.text = "Score: " + _score; private readonly StringBuilder _sb = new ();_sb.Clear(); _sb.Append("Score: " ); _sb.Append(_score); _scoreText.text = _sb.ToString(); var activeEnemies = enemies.Where(e => e.isActiveAndEnabled).ToList();var activeEnemies = new List<Enemy>();for (int i = 0 ; i < enemies.Count; i++){ if (enemies[i].isActiveAndEnabled) activeEnemies.Add(enemies[i]); } string msg = string .Format("Damage: {0}" , damage);
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 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 以下,性能抖动几乎不可感知。
下一篇将讲解 代码性能优化 ,从数据结构选择和算法层面进一步提升运行时效率。