💾 Unity 内存优化完全手册:从原理到实践的内存管理艺术
💡 内存优化的价值:
- 游戏内存占用过高,经常被系统杀死?
- 想理解 Unity 内存管理机制,优化无从下手?
- GC、托管内存、原生内存,傻傻分不清楚?
- 如何从根本上解决内存问题?
这篇文章! 将深入解析 Unity 内存管理机制,从 GC 原理到内存分类,从碎片化问题到实战优化策略,让内存更高效!
一、内存分类概述
1.1 Unity 内存管理结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Unity 内存架构: ┌─────────────────────────────────────┐ │ Unity 引擎 (C++) │ │ ┌─────────────────────────────┐ │ │ │ Native Memory │ │ │ │ - Scene │ │ │ │ - Audio │ │ │ │ - Code Size │ │ │ │ - Texture/Mesh │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Managed Memory (托管堆) │ │ │ │ - Mono/IL2CPP VM │ │ │ │ - C# 对象 │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ 第三方库 (不可检测) │ │ │ │ - Lua VM │ │ │ │ - Native 插件 │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘
|
1.2 四种内存类型
| 类型 |
说明 |
检测方式 |
| Unity Native |
C++ 层内存,Scene/Audio/Texture 等 |
✅ Profiler 可检测 |
| Managed Heap |
C# 托管堆,Mono/IL2CPP 管理 |
✅ Profiler 可检测 |
| Code Size |
代码文件内存,C#/C++/Shader |
✅ Profiler 可检测 |
| 第三方库 |
Lua/Native 插件 |
❌ Unity 无法检测 |
二、GC 机制详解
2.1 Boehm GC 特性
| 特性 |
说明 |
影响 |
| Non-generational |
非分代式 |
扫描整个堆 |
| Non-compacting |
非压缩式 |
内存碎片化 |
| Stop The World |
暂停所有线程 |
主线程卡顿 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 非分代式 GC: ┌─────────────────────────────────────┐ │ 传统分代式: │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ Gen0│ │ Gen1│ │ Gen2│ │ │ │小内存│ │中内存│ │长内存│ │ │ └─────┘ └─────┘ └─────┘ │ │ │ │ Boehm (Unity): │ │ ┌─────────────────────────────┐ │ │ │ 统一大内存池 │ │ │ │ 全部扫描回收 │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘
|
2.2 GC 机制考量
| 考量因素 |
说明 |
| Throughput |
一次回收能回收多少内存 |
| Pause times |
对主线程的影响时长 |
| Fragmentation |
回收后内存碎片化程度 |
| Mutator overhead |
回收本身的额外消耗 |
| Scalability |
多核多线程的扩展性 |
| Portability |
跨平台可移植性 |
2.3 GC 工作流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| GC 执行流程: ┌─────────────────────────────────────┐ │ 1. 挂起所有正在运行的线程 │ │ │ │ 2. 检查堆上的每个对象 │ │ │ │ 3. 搜索当前对象的所有引用 │ │ │ │ 4. 标记未被引用的对象为可删除 │ │ │ │ 5. 删除被标记的对象,释放内存 │ │ │ │ 6. 重定位剩余对象,更新指针引用 │ │ ( Boehm 不执行此步骤 ) │ └─────────────────────────────────────┘
|
2.4 GC 触发时机
| 触发条件 |
说明 |
| 内存不足 |
分配时发现可用空间不足 |
| 手动调用 |
System.GC.Collect() |
| 自动触发 |
Unity 不定期触发 |
| 间隔时间 |
一般 4-10 秒执行一次 |
三、内存碎片化问题
3.1 碎片化示意图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 内存碎片化: ┌─────────────────────────────────────┐ │ 压缩式 GC (理想): │ │ ┌─────┬─────┬─────┬─────┐ │ │ │对象A│对象B│对象C│空闲 │ │ │ └─────┴─────┴─────┴─────┘ │ │ ↓ 回收后重新排列 │ │ ┌─────┬─────┬─────────────┐ │ │ │对象A│对象B│ 大块空闲 │ │ │ └─────┴─────┴─────────────┘ │ │ │ │ Boehm GC (实际): │ │ ┌─────┬─────┬─────┬─────┐ │ │ │对象A│对象B│对象C│空闲 │ │ │ └─────┴─────┴─────┴─────┘ │ │ ↓ 回收后 (不压缩) │ │ ┌─────┬─────┬─┬───┬─────┐ │ │ │对象A│对象B│空│空 │空闲 │ │ │ └─────┴─────┴─┴───┴─────┘ │ │ → 碎片无法合并,产生碎片化 │ └─────────────────────────────────────┘
|
3.2 碎片化问题
| 问题 |
说明 |
解决方案 |
| 内存下降但池上升 |
碎片化导致无法利用空闲空间 |
先大后小分配 |
| Zombie Memory |
只用一次的内存占用 |
及时 Destroy |
| 内存泄漏 |
无法访问但未释放 |
追踪引用关系 |
四、值类型与引用类型
4.1 C# 类型分类
| 类型分类 |
包含类型 |
| 值类型 |
bool, byte, char, decimal, double, enum, float, int, long, sbyte, short, struct |
| 引用类型 |
string, StringBuilder, class, interface, delegate |
| 指针类型 |
引用类型的引用(指针) |
| 指令类型 |
变量声明、运算、跳转等 |
4.2 值类型 vs 引用类型
| 对比参数 |
值类型 |
引用类型 |
| 内存分配 |
线程栈 |
托管堆 |
| 内存回收 |
直接释放 |
等待 GC |
| new 实例 |
返回值本身 |
返回内存地址 |
| 变量赋值 |
逐字段复制 |
赋值内存地址 |
| 类型特点 |
轻量、无额外字段 |
需要额外字段 |
| 是否支持继承 |
不支持(密封) |
单继承 |
| 接口实现 |
支持 |
支持 |
| 表现方式 |
未装箱/已装箱 |
总是已装箱 |
4.3 栈与堆的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 栈 vs 堆: ┌─────────────────────────────────────┐ │ 栈 (Stack): │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │函数A│ │函数B│ │函数C│ │ │ └─────┘ └─────┘ └─────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ 局部变量 局部变量 局部变量 │ │ (值类型) (值类型) (值类型) │ │ │ │ 特点: │ │ • 快速分配和释放 │ │ • 线程独立 │ │ • 大小有限 │ └─────────────────────────────────────┘
┌─────────────────────────────────────┐ │ 堆 (Heap): │ │ ┌─────────────────────────────┐ │ │ │ 对象A (引用类型) │ │ │ │ ├─ 字段1 │ │ │ │ ├─ 字段2 │ │ │ │ └─ 字段3 │ │ │ └─────────────────────────────┘ │ │ ┌─────────────────────────────┐ │ │ │ 对象B (引用类型) │ │ │ │ ├─ 字段1 │ │ │ │ └─ 字段2 │ │ │ └─────────────────────────────┘ │ │ │ │ 特点: │ │ • GC 管理回收 │ │ • 可以存放大量数据 │ │ • 分配和释放较慢 │ └─────────────────────────────────────┘
|
4.4 值类型分配位置
💡 重要:值类型不一定分配在栈上!
1 2 3 4 5 6 7 8 9 10 11
| void MyFunction() { int value = 10; }
class MyClass { int value = 10; }
|
五、Native Memory 优化
5.1 Native Memory 组成
| 分类 |
说明 |
优化方向 |
| Scene |
GameObject 和 Component |
减少 GameObject 数量 |
| Audio |
音频资源和 DSP Buffer |
Force to mono,压缩格式 |
| Code Size |
代码和泛型模板 |
减少泛型滥用 |
| Texture |
纹理和 Upload Buffer |
压缩格式,关闭 R/W |
| Mesh |
网格数据 |
关闭不必要的 R/W |
5.2 Scene 优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Scene 内存占用: ┌─────────────────────────────────────┐ │ GameObject │ │ ┌─────────────────────────────┐ │ │ │ Unity C++ 层 │ │ │ │ ├─ Object 1 (Transform) │ │ │ │ ├─ Object 2 (Renderer) │ │ │ │ ├─ Object 3 (Collider) │ │ │ │ └─ ... (其他组件) │ │ │ └─────────────────────────────┘ │ │ │ │ 每个 GameObject = 多个 C++ 对象 │ │ → GameObject 越多,Native 越高 │ └─────────────────────────────────────┘
|
5.3 Audio 优化
| 设置 |
说明 |
建议 |
| Force to Mono |
强制单声道 |
95% 声音左右相同,可节省 50% |
| DSP Buffer |
声音缓冲大小 |
测试平衡延迟和 CPU 消耗 |
| Format |
压缩格式 |
iOS 用 MP3,Android 用 Vorbis |
5.4 Code Size 优化
1 2 3 4 5 6 7 8 9 10
| public class GenericData<T1, T2, T3, T4> { } public class A<T> { } public class B<T> { }
public class DataContainer { }
|
六、AssetBundle 优化
6.1 TypeTree 优化
| 设置 |
效果 |
建议 |
| 启用 TypeTree |
版本兼容,内存/包体大 |
需要版本兼容时 |
| 禁用 TypeTree |
内存/包体小,构建快 |
版本一致时 |
6.2 压缩方式对比
| 方式 |
压缩率 |
速度 |
内存 |
建议 |
| LZ4 |
较低 |
很快 |
低 |
✅ 推荐 |
| LZMA |
高 |
很慢 |
高 |
❌ 不推荐 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| LZ4 vs LZMA: ┌─────────────────────────────────────┐ │ LZMA (Steam Based): │ │ ┌─────────────────────────────┐ │ │ │ 全部解压 → 使用 │ │ │ │ 一次性占用大量内存 │ │ │ └─────────────────────────────┘ │ │ │ │ LZ4 (Chunk Based): │ │ ┌─────┬─────┬─────┬─────┐ │ │ │Chunk1│Chunk2│Chunk3│Chunk4│ │ │ └──┬──┴──┬──┴──┬──┴──┬──┘ │ │ │ 按需解压,重用内存 │ │ │ │ → 内存峰值更低,速度更快 │ └─────────────────────────────────────┘
|
6.3 包体大小建议
| 类型 |
建议大小 |
说明 |
| 单个 AB 包 |
1-2 MB |
平衡网络带宽和头开销 |
| Resource 文件夹 |
❌ 不用 |
红黑树不可卸载,拖慢启动 |
七、托管内存优化
7.1 VM 内存池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| VM 内存池管理: ┌─────────────────────────────────────┐ │ Block (内存块) │ │ ┌─────┬─────┬─────┬─────┐ │ │ │ Block1│Block2│Block3│... │ │ │ └──┬──┴──┬──┴──┬──┴──┬──┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ 访问状态追踪: │ │ • 连续 6 次 GC 未访问 │ │ → 返还给 OS │ │ │ │ Boehm: 只升不降 │ │ IL2CPP: 可以返回给 OS │ └─────────────────────────────────────┘
|
7.2 Incremental GC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 传统 GC vs Incremental GC: ┌─────────────────────────────────────┐ │ 传统 GC: │ │ ┌─────────────────────────────┐ │ │ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ │ │ │▓ 一帧完成,主线程卡死 │ │ │ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ │ │ └─────────────────────────────┘ │ │ │ │ Incremental GC: │ │ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ │ │ │▓│▓│▓│▓│▓│▓│▓│▓│▓│▓│ │ │ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘ │ │ 帧1 帧2 帧3... │ │ │ │ → 分帧执行,避免卡顿 │ └─────────────────────────────────────┘
|
八、内存优化最佳实践
8.1 编码规范
| 规范 |
说明 |
| Don’t Null it, Destroy it |
显式调用 Destroy,而非设为 null |
| Class vs Struct |
轻量对象优先使用 Struct |
| Pool In Pool |
高频小部件建立自己的内存池 |
| 避免闭包/匿名函数 |
都会生成类,占用内存 |
8.2 协程优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| IEnumerator Start() { while (true) { yield return null; } }
IEnumerator RunTask() { yield return null; }
void StartTask() { StartCoroutine(RunTask()); }
|
8.3 配置表优化
1 2 3 4 5 6 7 8
| Dictionary<int, ItemData> allItems = LoadAllItems();
ItemData GetItem(int id) { return NativeAPI.GetItemData(id); }
|
8.4 Singleton 慎用
| 问题 |
说明 |
| 生命周期长 |
从游戏开始到结束一直占用内存 |
| 难以释放 |
依赖关系复杂 |
| 建议 |
仅必要时使用,或实现可释放的单例 |
九、常见内存问题与解决
9.1 内存分配优化
| 问题 |
解决方案 |
| 频繁 Update 中分配 |
判断变化再分配,或使用计时器 |
| 重复创建集合 |
缓存引用,使用 Clear() |
| 闭包产生 GC |
避免在循环中创建 |
| 装箱拆箱 |
避免值类型当引用类型用 |
| String 操作 |
使用 StringBuilder |
| Unity API 分配 |
缓存返回值,使用 NonAlloc 版本 |
9.2 Unity API 优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| mesh.vertices; mesh.normals;
Vector3[] vertices = mesh.vertices; Vector3[] normals = mesh.normals;
string name = gameObject.name; string tag = gameObject.tag;
int instanceID = gameObject.GetInstanceID(); bool isPlayer = gameObject.CompareTag("Player");
Collider[] hits = Physics.OverlapSphere(center, radius);
Collider[] hits = new Collider[10]; int count = Physics.OverlapSphereNonAlloc(center, radius, hits);
|
十、优化检查清单
10.1 内存优化重点
| 优先级 |
类型 |
目标值 |
| 1 |
Reserved Total |
<80 MB |
| 2 |
托管内存 |
尽量降低 |
| 3 |
Texture2D |
<50 MB |
| 4 |
Mesh |
<20 MB |
| 5 |
RenderTexture |
控制分辨率 |
| 6 |
AssetBundle |
<50 个 |
10.2 综合检查清单
十一、参考资料
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com