为什么 Unity 的 GC 总在关键帧掉链子?深度解析 Managed/Native 内存模型
适合人群:Unity 中高级开发者 | 预计阅读时间:20分钟
我能帮你什么:彻底理解 Unity 的双层内存架构、GC 卡顿原因、内存泄漏排查、优化策略
🎯 一、开篇:这些内存问题你中了几个?
- 为什么游戏运行一段时间后会突然卡顿?(GC Spike)
- Profiler 显示内存占用 500MB,但系统监控显示 2GB?
- 调用了
Destroy(obj)但内存没有释放? Resources.UnloadUnusedAssets()为什么这么慢?- Mono 和 IL2CPP 的内存管理有什么区别?
如果你也被这些问题困扰,这篇文章将带你深入 Unity 的内存系统底层,彻底搞懂双层内存架构。
📖 二、原理研究:Unity 的双层内存架构
2.1 生活类比:两个仓库的管理模式
想象一个游戏公司有两个仓库:
仓库A(托管内存 - Managed Memory):
- 管理方式:自动化仓库,有机器人定期清理(GC)
- 存储内容:C# 脚本的临时数据(变量、对象)
- 特点:
- 放东西很快(分配快)
- 机器人会定期巡查,清理不用的东西(GC)
- 但机器人工作时会暂停所有业务(Stop-The-World)
仓库B(原生内存 - Native Memory):
- 管理方式:手动管理,需要明确申请和释放
- 存储内容:Texture、Mesh、Audio、AssetBundle 等引擎资源
- 特点:
- 放东西较慢(分配慢)
- 需要手动清理(调用 Unload/Destroy)
- 不会触发 GC,但需要你自己管理生命周期
关键问题:两个仓库之间的引用关系是内存泄漏的最大隐患!
2.2 Unity 内存架构全景图
┌─────────────────────────────────────────────────────────────┐
│ Operating System │
│ (物理内存 RAM) │
└───────────────────────┬─────────────────────────────────────┘
│
┌───────────────┴───────────────┐
↓ ↓
┌──────────────────────┐ ┌──────────────────────┐
│ Managed Memory │ │ Native Memory │
│ (C# Heap) │ │ (C++ Heap) │
├──────────────────────┤ ├──────────────────────┤
│ │ │ │
│ ┌────────────────┐ │ │ ┌────────────────┐ │
│ │ C# Objects │ │ │ │ Unity Engine │ │
│ │ - List<T> │ │ │ │ - GameObject │ │
│ │ - Dictionary │ │ │ │ - Transform │ │
│ │ - Custom Class │ │ │ │ - Renderer │ │
│ │ - String │ │ │ └────────────────┘ │
│ └────────────────┘ │ │ │
│ │ │ ┌────────────────┐ │
│ ┌────────────────┐ │ │ │ Assets │ │
│ │ MonoBehaviour │◄┼────┼─►│ - Texture │ │
│ │ (C# Wrapper) │ │ │ │ - Mesh │ │
│ └────────────────┘ │ │ │ - AudioClip │ │
│ │ │ │ - Material │ │
│ GC 管理: │ │ │ - AssetBundle │ │
│ - Incremental GC │ │ └────────────────┘ │
│ - Boehm GC │ │ │
│ (Mono/IL2CPP) │ │ 手动管理: │
│ │ │ - Destroy() │
└──────────────────────┘ │ - Unload() │
│ - DestroyImmediate()│
└──────────────────────┘
↑ ↑
│ │
C# 代码可见 部分可见
(Profiler: Mono 内存) (Profiler: Unity 内存)
关键概念:
-
Managed Memory(托管内存):
- 由 C# 运行时(Mono 或 IL2CPP)管理
- 自动垃圾回收(GC)
- 包含所有 C# 对象、值类型装箱、字符串等
-
Native Memory(原生内存):
- 由 Unity C++ 引擎管理
- 需要手动释放
- 包含所有引擎资源和 GameObject
-
Wrapper 机制:
- C# 中的
GameObject只是一个"引用句柄"(Managed) - 真正的数据存储在 Native 层
- 这就是为什么
Destroy(obj)后,obj != null可能仍为true(C# Wrapper 还在)
- C# 中的
源码路径:
Unity/Runtime/Allocator/MemoryManager.cpp- Native 内存管理Unity/Runtime/Mono/MonoManager.cpp- Mono 内存管理Unity/Runtime/Scripting/ScriptingTypes.cpp- C#/C++ 互操作
2.3 Managed Memory:垃圾回收机制详解
GC 算法演进
Unity 使用的 GC 算法:
| Unity 版本 | Mono GC | IL2CPP GC | 特性 |
|---|---|---|---|
| Unity 2018 及以前 | Boehm GC | Boehm GC | 非分代、Stop-The-World、标记-清除 |
| Unity 2019+ | Boehm GC | Boehm GC | 支持 Incremental GC(增量 GC) |
| Unity 2021+ | Boehm GC | Boehm GC | 优化的 Incremental GC + 时间切片 |
Boehm GC 工作流程:
[1. 分配阶段]
C# 代码创建对象 → 在 Managed Heap 分配内存
↓
Heap 空间不足?
├─ No → 继续分配
└─ Yes → 触发 GC
[2. 标记阶段 (Mark)]
暂停所有线程 (Stop-The-World)
↓
从 GC Roots 开始标记:
- Static 字段
- 栈上的局部变量
- MonoBehaviour 实例(被 Native 引用)
↓
递归标记所有可达对象
[3. 清除阶段 (Sweep)]
遍历整个 Heap
↓
未被标记的对象 → 回收内存
↓
恢复线程执行
[4. 结果]
⚠️ 卡顿!(可能 10-100ms)
Incremental GC(增量 GC):
传统 GC:
┌─────────────────────────────────────┐
│ Frame 1 │ Frame 2 │ GC (50ms) │ ...
└─────────────────────────────────────┘
↑
卡顿!
增量 GC:
┌─────────────────────────────────────┐
│ Frame 1 │ GC(5ms) │ Frame 2 │ GC(5ms) │ Frame 3 │ GC(5ms) │ ...
└─────────────────────────────────────┘
↑ ↑ ↑
分散到多帧,每次只卡顿一小部分
配置增量 GC:
// 在游戏启动时配置
void Awake()
{
// Unity 2019.1+
// GarbageCollector.GCMode = GarbageCollector.Mode.Enabled; // 默认模式
// 增量 GC 时间切片(单位:纳秒)
// 默认 3ms,可以根据目标帧率调整
GarbageCollector.incrementalTimeSliceNanoseconds = 3 * 1000 * 1000; // 3ms
Debug.Log($"GC Mode: {GarbageCollector.GCMode}");
}
2.4 Native Memory:手动内存管理
常见 Native 资源及其内存占用
// 1. Texture2D
Texture2D tex = new Texture2D(1024, 1024, TextureFormat.RGBA32, false);
// Native 内存: 1024 * 1024 * 4 bytes = 4MB
// Managed 内存: ~100 bytes (Wrapper 对象)
// 2. Mesh
Mesh mesh = new Mesh();
mesh.vertices = new Vector3[10000]; // 10000 * 12 bytes = 120KB
mesh.triangles = new int[30000]; // 30000 * 4 bytes = 120KB
// Native 内存: ~240KB
// Managed 内存: vertices/triangles 数组也在 Managed(临时)
// 3. AudioClip
AudioClip clip = Resources.Load<AudioClip>("BGM");
// Native 内存: 取决于音频长度和压缩格式
// 例如:3分钟 MP3(Compressed in Memory) → ~5MB
// 3分钟 WAV(Decompress on Load) → ~30MB
// 4. AssetBundle
AssetBundle bundle = AssetBundle.LoadFromFile("path");
// Native 内存: Bundle 文件大小(如果是 LZ4)
// 或者解压后的大小(如果是 LZMA)
// 5. GameObject
GameObject go = new GameObject();
go.AddComponent<MeshRenderer>();
go.AddComponent<MeshFilter>();
// Native 内存:
// - GameObject: ~200 bytes
// - Transform: ~150 bytes
// - MeshRenderer: ~100 bytes
// - MeshFilter: ~50 bytes
// 总计: ~500 bytes
Destroy vs DestroyImmediate
// Destroy: 延迟销毁(推荐)
Destroy(gameObject);
// - Native 对象在帧末尾销毁
// - C# Wrapper 等待下次 GC 回收
// - 安全,不会影响当前帧的引用
// DestroyImmediate: 立即销毁(危险)
DestroyImmediate(gameObject);
// - 立即销毁 Native 对象
// - C# Wrapper 立即标记为无效
// - 可能导致其他代码访问已销毁对象(NullReferenceException)
// - 仅在编辑器工具中使用!
Resources.UnloadUnusedAssets 的高成本
IEnumerator UnloadAssets()
{
// 这个方法非常昂贵!
AsyncOperation op = Resources.UnloadUnusedAssets();
yield return op;
// 内部流程:
// 1. 扫描所有 Native 对象(可能几万个)
// 2. 检查每个对象是否被 Managed 引用
// 3. 检查是否被其他 Native 对象引用
// 4. 卸载未被引用的对象
// 5. 触发一次 GC(确保 Managed 引用已清理)
// 耗时:100-500ms(取决于资源数量)
}
// 优化方案:控制调用时机
public class AssetManager : MonoBehaviour
{
private bool isUnloading = false;
public void UnloadWhenSafe()
{
if (!isUnloading)
{
StartCoroutine(UnloadRoutine());
}
}
IEnumerator UnloadRoutine()
{
isUnloading = true;
// 在加载屏幕或非游戏时段调用
yield return new WaitForSeconds(1f); // 等待引用释放
System.GC.Collect(); // 先手动 GC,清理 Managed 引用
yield return null;
yield return Resources.UnloadUnusedAssets(); // 再卸载 Native 资源
isUnloading = false;
Debug.Log("资源卸载完成");
}
}
🧪 三、实验验证:内存分析实战
实验1:使用 Memory Profiler 分析内存分布
using UnityEngine;
using System.Collections.Generic;
public class MemoryTest : MonoBehaviour
{
// Managed 内存测试
private List<byte[]> managedList = new List<byte[]>();
// Native 内存测试
private List<Texture2D> nativeList = new List<Texture2D>();
[ContextMenu("Allocate Managed Memory (10MB)")]
void AllocateManaged()
{
for (int i = 0; i < 10; i++)
{
byte[] data = new byte[1024 * 1024]; // 1MB
managedList.Add(data);
}
Debug.Log($"已分配 Managed 内存: {managedList.Count * 1}MB");
Debug.Log($"Mono 内存: {UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() / 1024f / 1024f:F2}MB");
}
[ContextMenu("Allocate Native Memory (10MB)")]
void AllocateNative()
{
for (int i = 0; i < 10; i++)
{
Texture2D tex = new Texture2D(512, 512, TextureFormat.RGBA32, false);
// 512 * 512 * 4 = 1MB per texture
nativeList.Add(tex);
}
Debug.Log($"已分配 Native 内存: {nativeList.Count * 1}MB");
Debug.Log($"Total Allocated: {UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / 1024f / 1024f:F2}MB");
}
[ContextMenu("Clear Managed Memory")]
void ClearManaged()
{
managedList.Clear();
Debug.Log("Managed 列表已清空(等待 GC)");
Debug.Log($"Mono 内存(清空前): {UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() / 1024f / 1024f:F2}MB");
System.GC.Collect();
Debug.Log($"Mono 内存(GC 后): {UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() / 1024f / 1024f:F2}MB");
}
[ContextMenu("Clear Native Memory")]
void ClearNative()
{
foreach (var tex in nativeList)
{
Destroy(tex);
}
nativeList.Clear();
Debug.Log("Native 资源已销毁");
Debug.Log($"Total Allocated: {UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / 1024f / 1024f:F2}MB");
}
}
实验步骤:
- 打开 Window → Profiler → Memory
- 打开 Package Manager → 安装 Memory Profiler
- 创建空对象,添加
MemoryTest脚本 - 右键点击脚本 → Allocate Managed Memory
- 观察 Profiler:
- Memory → Total Allocated: 增加约 10MB
- Memory → GC Allocated: 增加约 10MB
- 右键点击 → Allocate Native Memory
- 观察 Profiler:
- Total Allocated: 再增加约 10MB
- GC Allocated: 几乎不变(只有 Wrapper 对象)
- 打开 Memory Profiler → Capture → 分析内存快照
实验结果:
Managed Memory:
┌────────────────────────────────────┐
│ Managed Objects │
├────────────────────────────────────┤
│ byte[] (1MB) x 10 = 10MB │
│ List<T> 内部数组 = 0.1MB │
│ MonoBehaviour Wrapper = 0.001MB │
└────────────────────────────────────┘
Total: ~10.1MB (会被 GC 回收)
Native Memory:
┌────────────────────────────────────┐
│ Native Objects │
├────────────────────────────────────┤
│ Texture2D (1MB) x 10 = 10MB │
│ Texture2D Wrapper x 10= 0.001MB │
└────────────────────────────────────┘
Total: ~10MB (需要手动 Destroy)
实验2:GC Spike 复现与优化
using UnityEngine;
using System.Collections.Generic;
public class GCSpikeTest : MonoBehaviour
{
void Update()
{
// ❌ 错误做法:每帧创建临时对象
BadExample();
// ✅ 正确做法:复用对象
// GoodExample();
}
void BadExample()
{
// 每帧创建 List(Managed 分配)
List<Vector3> positions = new List<Vector3>();
for (int i = 0; i < 100; i++)
{
positions.Add(new Vector3(i, 0, 0));
}
// 字符串拼接(每次 + 都创建新字符串)
string result = "";
for (int i = 0; i < 10; i++)
{
result += i.ToString(); // ⚠️ 大量 GC Alloc
}
// 装箱(值类型 → 引用类型)
object boxed = 123; // ⚠️ 装箱分配
}
// 优化方案
private List<Vector3> _cachedPositions = new List<Vector3>(100);
private System.Text.StringBuilder _stringBuilder = new System.Text.StringBuilder(100);
void GoodExample()
{
// 复用 List
_cachedPositions.Clear();
for (int i = 0; i < 100; i++)
{
_cachedPositions.Add(new Vector3(i, 0, 0));
}
// 使用 StringBuilder
_stringBuilder.Clear();
for (int i = 0; i < 10; i++)
{
_stringBuilder.Append(i);
}
string result = _stringBuilder.ToString();
// 避免装箱
int value = 123; // 直接使用值类型
}
}
性能对比(使用 Profiler → CPU → GC.Alloc):
| 方法 | GC Alloc/帧 | GC 频率(60FPS) |
|---|---|---|
| BadExample | ~5KB | 每 10 秒一次 GC |
| GoodExample | ~0B | 每 60 秒一次 GC |
实验3:内存泄漏检测
using UnityEngine;
using System.Collections.Generic;
public class MemoryLeakTest : MonoBehaviour
{
// ⚠️ 泄漏场景1:静态集合持有引用
private static List<GameObject> s_AllEnemies = new List<GameObject>();
// ⚠️ 泄漏场景2:事件未解绑
public static event System.Action OnGameEnd;
void Start()
{
// 场景1:敌人被销毁,但 List 还持有引用
GameObject enemy = new GameObject("Enemy");
s_AllEnemies.Add(enemy);
Destroy(enemy); // ⚠️ enemy 的 Native 对象被销毁
// 但 Managed Wrapper 还在 List 中
// 导致 GC 无法回收
// 场景2:订阅事件但忘记解绑
OnGameEnd += HandleGameEnd; // ⚠️ 如果 MonoBehaviour 被销毁
// 但事件未解绑,依然被引用
}
void HandleGameEnd()
{
Debug.Log("Game Ended");
}
void OnDestroy()
{
// ✅ 正确做法:清理引用
s_AllEnemies.Remove(gameObject);
OnGameEnd -= HandleGameEnd;
}
// 内存泄漏检测工具
[ContextMenu("Check Memory Leaks")]
void CheckLeaks()
{
Debug.Log("=== 内存泄漏检测 ===");
// 检测1:静态集合中的无效引用
int invalidCount = 0;
foreach (var obj in s_AllEnemies)
{
if (obj == null) // Unity 的 null 检查(检查 Native 是否被销毁)
{
invalidCount++;
}
}
if (invalidCount > 0)
{
Debug.LogWarning($"发现 {invalidCount} 个无效引用(内存泄漏)");
}
// 检测2:事件订阅数量
if (OnGameEnd != null)
{
int subscriberCount = OnGameEnd.GetInvocationList().Length;
Debug.Log($"OnGameEnd 事件订阅者数量: {subscriberCount}");
}
}
}
使用 Memory Profiler 检测泄漏:
// 1. 在 Memory Profiler 中拍摄两个快照
// 2. 使用 "Compare" 功能对比
// 3. 查看 "Managed Objects" → "Remaining" 列
// 4. 找出应该被回收但仍然存在的对象
🏗️ 四、优化策略:五大最佳实践
策略1:减少 GC Alloc
// ❌ 常见 GC Alloc 陷阱
public class BadPractices : MonoBehaviour
{
void Update()
{
// 1. 每帧创建数组
int[] array = new int[100]; // 每帧 400 bytes
// 2. 字符串拼接
string name = "Player_" + id; // 每次创建新字符串
// 3. 闭包捕获
StartCoroutine(MyCoroutine(() => {
Debug.Log(gameObject.name); // 闭包捕获 gameObject → GC Alloc
}));
// 4. LINQ 查询
var players = FindObjectsOfType<Player>().Where(p => p.isAlive).ToList();
// Where() 和 ToList() 都会分配内存
// 5. 装箱
Dictionary<int, object> dict = new Dictionary<int, object>();
dict[1] = 100; // int → object 装箱
}
}
// ✅ 优化方案
public class GoodPractices : MonoBehaviour
{
// 1. 对象池
private Queue<GameObject> _objectPool = new Queue<GameObject>();
GameObject GetFromPool()
{
if (_objectPool.Count > 0)
return _objectPool.Dequeue();
else
return Instantiate(prefab);
}
void ReturnToPool(GameObject obj)
{
obj.SetActive(false);
_objectPool.Enqueue(obj);
}
// 2. StringBuilder
private System.Text.StringBuilder _sb = new System.Text.StringBuilder();
string BuildString(int id)
{
_sb.Clear();
_sb.Append("Player_");
_sb.Append(id);
return _sb.ToString(); // 只有最后一次分配
}
// 3. 缓存数组
private Player[] _playersCache = new Player[100];
private int _playerCount;
void UpdatePlayers()
{
_playerCount = 0;
var allPlayers = FindObjectsOfType<Player>();
for (int i = 0; i < allPlayers.Length; i++)
{
if (allPlayers[i].isAlive)
{
_playersCache[_playerCount++] = allPlayers[i];
}
}
// 使用 _playersCache[0..._playerCount-1]
}
// 4. 避免装箱:使用泛型
private Dictionary<int, int> _typedDict = new Dictionary<int, int>();
}
策略2:优化 Native 资源加载
public class AssetLifecycleManager : MonoBehaviour
{
private Dictionary<string, AssetHandle> _loadedAssets = new Dictionary<string, AssetHandle>();
class AssetHandle
{
public Object asset;
public int refCount;
public float lastAccessTime;
}
public T LoadAsset<T>(string path) where T : Object
{
if (_loadedAssets.TryGetValue(path, out var handle))
{
handle.refCount++;
handle.lastAccessTime = Time.time;
return handle.asset as T;
}
// 首次加载
var asset = Resources.Load<T>(path);
_loadedAssets[path] = new AssetHandle
{
asset = asset,
refCount = 1,
lastAccessTime = Time.time
};
return asset;
}
public void ReleaseAsset(string path)
{
if (_loadedAssets.TryGetValue(path, out var handle))
{
handle.refCount--;
if (handle.refCount <= 0)
{
// 延迟卸载(避免频繁加载/卸载)
StartCoroutine(DelayedUnload(path));
}
}
}
IEnumerator DelayedUnload(string path)
{
yield return new WaitForSeconds(5f); // 5秒后再检查
if (_loadedAssets.TryGetValue(path, out var handle))
{
if (handle.refCount <= 0)
{
Resources.UnloadAsset(handle.asset);
_loadedAssets.Remove(path);
Debug.Log($"卸载资源: {path}");
}
}
}
// 定期清理长时间未使用的资源
void Update()
{
if (Time.frameCount % 300 == 0) // 每 300 帧检查一次
{
var toRemove = new List<string>();
foreach (var kvp in _loadedAssets)
{
if (kvp.Value.refCount <= 0 &&
Time.time - kvp.Value.lastAccessTime > 60f) // 60秒未使用
{
toRemove.Add(kvp.Key);
}
}
foreach (var path in toRemove)
{
Resources.UnloadAsset(_loadedAssets[path].asset);
_loadedAssets.Remove(path);
}
if (toRemove.Count > 0)
{
Debug.Log($"自动清理 {toRemove.Count} 个未使用资源");
}
}
}
}
策略3:Texture 内存优化
using UnityEngine;
public class TextureOptimizer : MonoBehaviour
{
[Header("Texture 优化配置")]
[SerializeField] private int maxTextureSize = 1024;
[SerializeField] private TextureFormat mobileFormat = TextureFormat.ETC2_RGBA8;
// 运行时压缩 Texture(移动平台)
public Texture2D OptimizeTexture(Texture2D source)
{
// 检查是否需要缩放
int newWidth = Mathf.Min(source.width, maxTextureSize);
int newHeight = Mathf.Min(source.height, maxTextureSize);
if (newWidth != source.width || newHeight != source.height)
{
// 缩放 Texture
Texture2D scaled = ScaleTexture(source, newWidth, newHeight);
// 压缩
scaled.Compress(true);
return scaled;
}
return source;
}
Texture2D ScaleTexture(Texture2D source, int width, int height)
{
RenderTexture rt = RenderTexture.GetTemporary(width, height);
Graphics.Blit(source, rt);
RenderTexture previous = RenderTexture.active;
RenderTexture.active = rt;
Texture2D result = new Texture2D(width, height);
result.ReadPixels(new Rect(0, 0, width, height), 0, 0);
result.Apply();
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(rt);
return result;
}
// 编辑器工具:批量优化项目中的 Texture
#if UNITY_EDITOR
[UnityEditor.MenuItem("Tools/Optimize All Textures")]
static void OptimizeAllTextures()
{
var textures = UnityEditor.AssetDatabase.FindAssets("t:Texture2D");
int optimized = 0;
foreach (var guid in textures)
{
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
var importer = UnityEditor.AssetImporter.GetAtPath(path) as UnityEditor.TextureImporter;
if (importer != null)
{
bool changed = false;
// 限制最大尺寸
if (importer.maxTextureSize > 2048)
{
importer.maxTextureSize = 2048;
changed = true;
}
// 设置压缩格式
var androidSettings = importer.GetPlatformTextureSettings("Android");
if (!androidSettings.overridden)
{
androidSettings.overridden = true;
androidSettings.format = UnityEditor.TextureImporterFormat.ETC2_RGBA8;
importer.SetPlatformTextureSettings(androidSettings);
changed = true;
}
if (changed)
{
importer.SaveAndReimport();
optimized++;
}
}
}
Debug.Log($"优化了 {optimized} 个 Texture");
}
#endif
}
策略4:对象池模式(终极优化)
using UnityEngine;
using System.Collections.Generic;
public class ObjectPool<T> where T : Component
{
private Queue<T> _pool = new Queue<T>();
private T _prefab;
private Transform _parent;
private int _initialSize;
public ObjectPool(T prefab, int initialSize = 10, Transform parent = null)
{
_prefab = prefab;
_initialSize = initialSize;
_parent = parent;
// 预分配
for (int i = 0; i < initialSize; i++)
{
var obj = Object.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
_pool.Enqueue(obj);
}
}
public T Get()
{
T obj;
if (_pool.Count > 0)
{
obj = _pool.Dequeue();
}
else
{
// 池子用完了,动态扩展
obj = Object.Instantiate(_prefab, _parent);
Debug.LogWarning($"对象池扩展:{typeof(T).Name}");
}
obj.gameObject.SetActive(true);
return obj;
}
public void Return(T obj)
{
obj.gameObject.SetActive(false);
_pool.Enqueue(obj);
}
public void Clear()
{
while (_pool.Count > 0)
{
var obj = _pool.Dequeue();
Object.Destroy(obj.gameObject);
}
}
}
// 使用示例
public class BulletManager : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
private ObjectPool<Bullet> _bulletPool;
void Awake()
{
_bulletPool = new ObjectPool<Bullet>(bulletPrefab, 50, transform);
}
public void SpawnBullet(Vector3 position, Vector3 direction)
{
var bullet = _bulletPool.Get();
bullet.transform.position = position;
bullet.Initialize(direction, () => _bulletPool.Return(bullet));
}
}
public class Bullet : MonoBehaviour
{
private System.Action _onDestroy;
public void Initialize(Vector3 direction, System.Action onDestroy)
{
_onDestroy = onDestroy;
// 初始化子弹逻辑
}
void OnCollisionEnter(Collision collision)
{
// 碰撞后回收
_onDestroy?.Invoke();
}
}
策略5:内存监控与预警系统
using UnityEngine;
using UnityEngine.Profiling;
public class MemoryMonitor : MonoBehaviour
{
[Header("内存阈值(MB)")]
[SerializeField] private float warningThreshold = 500f;
[SerializeField] private float criticalThreshold = 800f;
[Header("监控间隔(秒)")]
[SerializeField] private float checkInterval = 1f;
private float _lastCheckTime;
void Update()
{
if (Time.time - _lastCheckTime > checkInterval)
{
_lastCheckTime = Time.time;
CheckMemory();
}
}
void CheckMemory()
{
long totalMemory = Profiler.GetTotalAllocatedMemoryLong();
float totalMB = totalMemory / 1024f / 1024f;
if (totalMB > criticalThreshold)
{
Debug.LogError($"[内存危机] 当前内存: {totalMB:F2}MB,执行紧急清理");
EmergencyCleanup();
}
else if (totalMB > warningThreshold)
{
Debug.LogWarning($"[内存警告] 当前内存: {totalMB:F2}MB");
}
// 详细内存信息
if (Input.GetKeyDown(KeyCode.F1))
{
PrintDetailedMemoryInfo();
}
}
void EmergencyCleanup()
{
// 1. 强制 GC
System.GC.Collect();
// 2. 卸载未使用资源
Resources.UnloadUnusedAssets();
// 3. 清理对象池
// ObjectPoolManager.Instance.ClearAll();
// 4. 降低资源质量
QualitySettings.SetQualityLevel(0); // 切换到最低画质
Debug.Log("紧急清理完成");
}
void PrintDetailedMemoryInfo()
{
Debug.Log("===== 详细内存信息 =====");
Debug.Log($"Total Allocated: {Profiler.GetTotalAllocatedMemoryLong() / 1024f / 1024f:F2}MB");
Debug.Log($"Mono Used: {Profiler.GetMonoUsedSizeLong() / 1024f / 1024f:F2}MB");
Debug.Log($"Mono Heap: {Profiler.GetMonoHeapSizeLong() / 1024f / 1024f:F2}MB");
Debug.Log($"Total Reserved: {Profiler.GetTotalReservedMemoryLong() / 1024f / 1024f:F2}MB");
Debug.Log($"GFX Driver Allocated: {Profiler.GetAllocatedMemoryForGraphicsDriver() / 1024f / 1024f:F2}MB");
}
}
📊 五、技术图表
图表1:Managed vs Native 内存对比
游戏运行 30 分钟后的内存分布:
Managed Memory (GC 管理):
┌────────────────────────────────┐
│ MonoBehaviour 脚本 50MB │
│ List/Dictionary 等集合 30MB │
│ 字符串缓存 20MB │
│ 临时对象(等待 GC) 10MB │
├────────────────────────────────┤
│ 总计: 110MB │
│ GC 触发频率: 每 15 秒 │
└────────────────────────────────┘
Native Memory (手动管理):
┌────────────────────────────────┐
│ Texture2D 500MB │
│ Mesh 100MB │
│ AudioClip 150MB │
│ AssetBundle 80MB │
│ GameObject/Component 70MB │
│ RenderTexture 50MB │
├────────────────────────────────┤
│ 总计: 950MB │
│ 需要手动调用 Destroy/Unload │
└────────────────────────────────┘
系统内存占用: 110MB + 950MB + 100MB (引擎开销) = 1160MB
图表2:GC 优化前后对比
优化前(卡顿严重):
┌──────────────────────────────────────────────────────┐
│ Frame Timeline (60 FPS 目标 = 16.67ms/帧) │
├──────────────────────────────────────────────────────┤
│ Frame 1: 15ms │ Frame 2: 16ms │ GC: 80ms │ ... │
└──────────────────────────────────────────────────────┘
↑
卡顿 4-5 帧
优化后(使用增量 GC + 对象池):
┌──────────────────────────────────────────────────────┐
│ Frame 1: 15ms │ GC: 2ms │ Frame 2: 15ms │ GC: 2ms │ ..│
└──────────────────────────────────────────────────────┘
↑ ↑
分散到多帧,每次卡顿 < 3ms
🎓 六、总结与延伸
核心要点回顾
-
双层内存架构:
- Managed(C# Heap):自动 GC,但会卡顿
- Native(C++ Heap):手动管理,需要 Destroy/Unload
-
GC 卡顿优化:
- 启用增量 GC(Unity 2019+)
- 减少每帧 GC Alloc(对象池、StringBuilder、避免装箱)
- 控制 GC 触发时机(加载屏幕、非战斗时段)
-
Native 资源管理:
- 使用引用计数系统
- 延迟卸载避免频繁加载
- 定期清理未使用资源
- Texture 压缩和尺寸限制
-
内存泄漏预防:
- 静态集合定期清理无效引用
- 事件订阅必须解绑
- 使用 Memory Profiler 定期检查
延伸阅读清单
-
官方文档
-
深度文章
-
工具推荐
- Unity Memory Profiler
- Unity Profiler (CPU + Memory 模块)
- UWA 性能分析平台(国内)
-
本系列后续文章
- 《PlayerLoop 与自定义更新机制》
- 《Script Execution Order 背后的机制》
💡 互动思考题
- 为什么
Destroy(obj)后,obj == null返回true,但obj.ToString()不会报错? - 如果你的游戏有 1000 个敌人,应该如何设计对象池?
- IL2CPP 和 Mono 在内存管理上有什么本质区别?
欢迎在评论区分享你的内存优化经验!
下一篇预告:《PlayerLoop 与自定义更新机制:如何在 Update 之外运行代码?》
作者:[胡利光] | Unity 技术博主
专注于 Unity 底层原理与架构设计
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!
3088

被折叠的 条评论
为什么被折叠?



