【Unity 底层与原理向】05_Unity内存模型Managed与Native详解

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

为什么 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 内存)

关键概念

  1. Managed Memory(托管内存)

    • 由 C# 运行时(Mono 或 IL2CPP)管理
    • 自动垃圾回收(GC)
    • 包含所有 C# 对象、值类型装箱、字符串等
  2. Native Memory(原生内存)

    • 由 Unity C++ 引擎管理
    • 需要手动释放
    • 包含所有引擎资源和 GameObject
  3. Wrapper 机制

    • C# 中的 GameObject 只是一个"引用句柄"(Managed)
    • 真正的数据存储在 Native 层
    • 这就是为什么 Destroy(obj) 后,obj != null 可能仍为 true(C# Wrapper 还在)

源码路径

  • 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 GCIL2CPP GC特性
Unity 2018 及以前Boehm GCBoehm GC非分代、Stop-The-World、标记-清除
Unity 2019+Boehm GCBoehm GC支持 Incremental GC(增量 GC)
Unity 2021+Boehm GCBoehm 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");
    }
}

实验步骤

  1. 打开 Window → Profiler → Memory
  2. 打开 Package Manager → 安装 Memory Profiler
  3. 创建空对象,添加 MemoryTest 脚本
  4. 右键点击脚本 → Allocate Managed Memory
  5. 观察 Profiler:
    • Memory → Total Allocated: 增加约 10MB
    • Memory → GC Allocated: 增加约 10MB
  6. 右键点击 → Allocate Native Memory
  7. 观察 Profiler:
    • Total Allocated: 再增加约 10MB
    • GC Allocated: 几乎不变(只有 Wrapper 对象)
  8. 打开 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

🎓 六、总结与延伸

核心要点回顾

  1. 双层内存架构

    • Managed(C# Heap):自动 GC,但会卡顿
    • Native(C++ Heap):手动管理,需要 Destroy/Unload
  2. GC 卡顿优化

    • 启用增量 GC(Unity 2019+)
    • 减少每帧 GC Alloc(对象池、StringBuilder、避免装箱)
    • 控制 GC 触发时机(加载屏幕、非战斗时段)
  3. Native 资源管理

    • 使用引用计数系统
    • 延迟卸载避免频繁加载
    • 定期清理未使用资源
    • Texture 压缩和尺寸限制
  4. 内存泄漏预防

    • 静态集合定期清理无效引用
    • 事件订阅必须解绑
    • 使用 Memory Profiler 定期检查

延伸阅读清单

  1. 官方文档

  2. 深度文章

  3. 工具推荐

    • Unity Memory Profiler
    • Unity Profiler (CPU + Memory 模块)
    • UWA 性能分析平台(国内)
  4. 本系列后续文章

    • 《PlayerLoop 与自定义更新机制》
    • 《Script Execution Order 背后的机制》

💡 互动思考题

  1. 为什么 Destroy(obj) 后,obj == null 返回 true,但 obj.ToString() 不会报错?
  2. 如果你的游戏有 1000 个敌人,应该如何设计对象池?
  3. IL2CPP 和 Mono 在内存管理上有什么本质区别?

欢迎在评论区分享你的内存优化经验!


下一篇预告:《PlayerLoop 与自定义更新机制:如何在 Update 之外运行代码?》


作者:[胡利光] | Unity 技术博主
专注于 Unity 底层原理与架构设计
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值