💾 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> { }

// 每个具体类型都会在 C++ 层生成对应代码
// 导致 Code Size 激增

// ✅ 合理使用泛型
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
// ❌ 全部加载到 C#/Lua 内存
Dictionary<int, ItemData> allItems = LoadAllItems();

// ✅ C++ 管理,C#/Lua 查询接口
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;

// ❌ name/tag 产生 GC
string name = gameObject.name;
string tag = gameObject.tag;

// ✅ 使用替代方法
int instanceID = gameObject.GetInstanceID();
bool isPlayer = gameObject.CompareTag("Player");

// ❌ 返回新数组
Collider[] hits = Physics.OverlapSphere(center, radius);

// ✅ 使用 NonAlloc 版本
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 综合检查清单

  • Reserved Total < 80MB
  • 纹理资源 < 50MB
  • 网格资源 < 20MB
  • AssetBundle 冗余检查
  • 避免 foreach,使用 for
  • 字符串使用 StringBuilder
  • Struct 替代 Class(轻量对象)
  • 避免闭包和匿名函数
  • 缓存组件引用
  • 使用 CompareTag
  • 避免 SendMessage
  • 降低 Update 频率
  • 预生成 Wait 对象
  • 禁用不可见 GameObject
  • 图集 ≤1024×1024
  • 关闭 Mipmaps(UI)
  • 使用九宫格
  • 移除 Fill Center
  • 减少 Mask 使用
  • 动静分离

十一、参考资料


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com