Unity资源内存管理与释放

做过一段时间 Unity 开发后,你大概率遇到过这些问题:

场景切换时,内存只增不减,跑久了就崩溃

手机上玩着玩着突然「闪退」,日志里写着 OutOfMemory

长时间运行后明显卡顿、掉帧,尤其是频繁切场景或加载大关卡的游戏

背后核心问题就是:资源加载进来了,但没有正确释放。

一、先搞清楚:Unity 里的"内存"都是什么?

Unity 程序运行时,大致可以理解有三块主要内存:

C# 托管堆(Managed Heap)
  • 由 .NET / Mono 管理

  • 存放 C# 对象:

    • 你的脚本类实例(MonoBehaviour,普通 C# 类)

    • List<T>, Dictionary<K,V> 等容器

    • 字符串、数据结构等

  • 回收方式:C# 垃圾回收(GC),由 System.GC.Collect() 触发或自动运行

原生内存 / 引擎层内存(Native Memory)
  • 由 Unity 引擎(C++)管理

  • 存放各种 引擎资源对象(Asset) 的实际数据:纹理像素、网格顶点、音频数据、动画曲线等

  • 这些对象在 C# 中以 UnityEngine.Object 派生类表示,例如:Texture2D, Mesh, Material, AudioClip 等

图形显存(GPU Memory)
  • 由 GPU 占用

  • 文本贴图、Mesh 等通常要上传一份到 GPU

  • 一部分受 Quality / Graphics 设置影响,如 MipMap、压缩格式等

你在 C# 里操作的 Texture2D tex,只是一个指向引擎内存中真实纹理数据的"句柄"。

想要真正释放这类资源,不能只依靠 C# 垃圾回收,还需要 Unity 自己的资源卸载机制。

二、几个关键 API:谁负责释放什么?
1. Object.Destroy / Object.DestroyImmediate

作用:销毁场景中的对象 / 组件实例(GameObject、Component)。

cs 复制代码
Destroy(gameObject);
  • 只会销毁 实例(比如场景里的怪物、UI 元素)

  • 不会直接卸载它们背后的资源 Asset(Prefab、Texture、Material 等)

  • 如果某个资源 Asset 再也没有任何实例引用了,它才有机会被卸载(通常靠 Resources.UnloadUnusedAssets

2. Resources.UnloadAsset(Object assetToUnload)

作用:手动卸载指定的 Asset 资源,为原生内存"减负"。

cs 复制代码
Texture2D tex = Resources.Load<Texture2D>("Textures/HeroIcon");

// 用完后确定不再需要
Resources.UnloadAsset(tex);
tex = null;  // 方便 C# GC 回收托管引用

特点与限制:

  • 目标必须是Asset 类型对象 ,这类是原生内存对象:Texture2D, Sprite, Material, Mesh, AudioClip, AnimationClip

  • 不能用于 GameObject / Component / MonoBehaviour这类托管堆对象

  • 来源不限:

    • Resources.Load

    • 场景自动加载的资源

    • AssetBundle / Addressables 加载的资源

  • 如果资源仍然被使用(比如某个材质还在引用该贴图,或某个实例正在使用这个 Mesh),卸载会导致:

    • 贴图丢失、材质变粉、模型消失等

一般只在:明确知道某个资源后面绝不再用时 使用。

3. Resources.UnloadUnusedAssets()

作用:全局扫描所有资源,卸载"当前没有任何引用"的 Asset

cs 复制代码
System.GC.Collect();                  // 先做一次 C# GC(可选但推荐)
yield return Resources.UnloadUnusedAssets(); // 异步等待引擎资源回收完成

特点:

  • 类似"引擎层的垃圾回收":

    1. Unity 找出所有仍被引用的 Asset

    2. 对于没有引用的 Asset,释放其内存

  • 不关心资源是如何加载进来的:Resources / 场景 / AssetBundle 都可以

  • 返回 AsyncOperation,是异步执行的

  • 在移动端/低性能设备上,调用时可能会有短暂卡顿,不宜频繁调用

  • 典型调用时机:

    • 场景切换完成之后

    • 大关卡卸载、大章节结束

    • 长时间运行的游戏在安全区、主菜单等"低压力"节点

注意:它只卸载"没有引用 的资源"。

你代码里只要还挂着一个引用(比如某个 Singleton 脚本里有静态字段指向某材质),这个资源就不会被卸载。

4. AssetBundle.Unload(bool unloadAllLoadedObjects)

作用:释放 AssetBundle 自身和由它创建的资源。

cs 复制代码
bundle.Unload(false); // 或 true

参数说明:

  • false:只卸载 AssetBundle 本身 (压缩包数据、索引等),

    由它加载出的 Asset 仍然存在,继续占用内存;

    适合 "Bundle 只作为加载通道用,资源要长期留在内存" 的情况。

  • true:卸载 AssetBundle,并尝试卸载所有由这个 Bundle 加载的 Assets (如果不再被引用)。

    若还有引用,资源会顶着不卸,或导致引用失效,需谨慎使用。

Resources.Unload* 系列的关系:

  • AssetBundle.Unload 是 AssetBundle 级别的回收

  • Resources.UnloadAsset / Resources.UnloadUnusedAssets 是 Asset 级别的回收

  • 实战中常搭配使用:

    • 该 Bundle 的内容完全用完,不再需要 → bundle.Unload(true)

    • 或者先销毁所有实例、清空引用 → 再 Resources.UnloadUnusedAssets() 全局扫一遍

三、常见误区
1. 误区:只要 Destroy(GameObject) 就会完全释放内存

事实:

  • 只 Destroy 实例,不会一定释放背后的资源

  • 例如:你加载了一个超大的图集 Texture,实例化了 100 个 Image:

    • Destroy 这 100 个 Image,只是释放了这 100 个实例占用的内存

    • 纹理本身仍在内存中,直到:

      • 你不再有任何引用

      • 再调用 Resources.UnloadUnusedAssets() 或某些特定卸载操作

2. 误区:频繁 Resources.UnloadUnusedAssets() 就能保持内存特别低

问题:

  • Resources.UnloadUnusedAssets 开销不小:

    • 会扫描所有对象引用

    • 做原生资源的释放

    • 在移动端容易造成明显卡顿

  • 不适合:

    • 每帧、每几秒调用一次

    • 在战斗中、动画播放中频繁调用

建议:

  • 按阶段调用,例如:

    • 场景加载完成后

    • 大片段(章节)结束时

    • 玩家进入菜单、暂停界面时

3. 误区:只要 System.GC.Collect() 就能解决内存问题

注意:

  • GC.Collect 只能回收 C# 托管对象

  • 无法直接释放 Unity 的 Asset / 纹理 / Mesh / Audio 等引擎资源

  • 如果游戏主要内存压力来自贴图 / 音频等大资源,单靠 GC 是没用的

  • 甚至会带来自身的 CPU 开销和瞬时卡顿

3. 误区:混淆第三方资源管理插件的资源卸载跟Unity原生接口的卸载

如果你使用 Addressables(或YooAsset这种资源管理框架):

按照官方推荐使用 Addressables.LoadAssetAsync / Addressables.InstantiateAsync

对应的释放要用:

Addressables.Release(asset)

Addressables.ReleaseInstance(instance)

Addressables 内部会根据引用计数,在合适时机卸载 Asset / AssetBundle

避免手动对 Addressables 管理的资源调用 Resources.UnloadAsset / AssetBundle.Unload,避免打架。

附上一些测试代码:

测试卸载Resources.Load加载的资源

cs 复制代码
using UnityEngine;

public class TestUnload : MonoBehaviour
{
    
    public void LoadCube()
    {
        var asset = Resources.Load("Cube");
        var obj =GameObject.Instantiate(asset);
        obj.name = "Cube";
    }
    
    public void DestroyCube()
    {
        GameObject.Destroy(GameObject.Find("Cube")); //销毁GameObject
        Resources.UnloadUnusedAssets();//释放GameObject的依赖的资源
    }
}

测试卸载YooAsset框架加载的资源(AssetBundle中加载出来的模型预制体)

cs 复制代码
using System.Collections;
using Cysharp.Threading.Tasks;
using UnityEngine;
using YooAsset;

public class TestAbUnload : MonoBehaviour
{
    public GameObject parentObj;
    private GameObject model;
    private string _packageVersion;
    /// <summary>
    /// 资源系统运行模式
    /// </summary>
    public EPlayMode PlayMode = EPlayMode.EditorSimulateMode;
    private static bool isInitialize = false;
    
    // Start is called before the first frame update
    IEnumerator Start()
    {
#if UNITY_EDITOR
        //PlayMode = EPlayMode.EditorSimulateMode;
#elif UNITY_WEBGL
        PlayMode = EPlayMode.WebPlayMode;
#else
        //PlayMode = EPlayMode.HostPlayMode;
        PlayMode = EPlayMode.OfflinePlayMode;
#endif
        if (isInitialize)
            yield break;
        isInitialize = true;
        Debug.Log("启动游戏, 初始化资源系统... PlayMode:" + PlayMode);
        Application.targetFrameRate = 60;

        // 初始化资源系统
        YooAssets.Initialize();

        //开始补丁更新流程
        InitPackage();
         

        DontDestroyOnLoad(gameObject);
    }
    
    
    private async UniTask InitPackage()
    {
        var playMode = PlayMode;
        var packageName = "DefaultPackage";

        // 创建资源包裹类
        var package = YooAssets.TryGetPackage(packageName);
        if (package == null)
            package = YooAssets.CreatePackage(packageName);

        // 编辑器下的模拟模式
        InitializationOperation initializationOperation = null;
        if (playMode == EPlayMode.EditorSimulateMode)
        {
            var buildParam = new EditorSimulateBuildParam(packageName);
            var buildResult = EditorSimulateModeHelper.SimulateBuild(buildParam);
            var packageRoot = buildResult.PackageRootDirectory;
            var createParameters = new EditorSimulateModeParameters();
            createParameters.EditorFileSystemParameters = FileSystemParameters.CreateDefaultEditorFileSystemParameters(packageRoot);
            initializationOperation = package.InitializeAsync(createParameters);
        }

        // 单机运行模式
        if (playMode == EPlayMode.OfflinePlayMode)
        {
            var createParameters = new OfflinePlayModeParameters();
            createParameters.BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
            initializationOperation = package.InitializeAsync(createParameters);
        }
        

        await initializationOperation.ToUniTask();

        // 如果初始化失败弹出提示界面
        if (initializationOperation.Status != EOperationStatus.Succeed)
        {
            Debug.LogWarning($"{initializationOperation.Error}");
        }
        else
        {
            Debug.Log("初始化资源系统成功");
        }

        await UpdatePackageVersion(packageName).ToUniTask();
        await UpdateManifest(packageName).ToUniTask();
        
        //设置默认的资源包
        var gamePackage = YooAssets.GetPackage(packageName);
        YooAssets.SetDefaultPackage(gamePackage);
        
        LoadModel();
    }
    
    private IEnumerator UpdatePackageVersion(string packageName)
    {
        var package = YooAssets.GetPackage(packageName);
        var operation = package.RequestPackageVersionAsync();
        yield return operation;

        if (operation.Status != EOperationStatus.Succeed)
        {
            Debug.LogError(operation.Error);
        }
        else
        {
            _packageVersion = operation.PackageVersion;
            Debug.Log($"Request package version : {_packageVersion}");
        }
    }
    
    private IEnumerator UpdateManifest(string packageName)
    {
        var package = YooAssets.GetPackage(packageName);
        var operation = package.UpdatePackageManifestAsync(_packageVersion);
        yield return operation;

        if (operation.Status != EOperationStatus.Succeed)
        {
            Debug.LogError(operation.Error);
        }
        else
        {
            Debug.Log($"Update package manifest success");
        }
    }


    private AssetHandle _assetHandle;
    public void LoadModel()
    {
        Debug.Log("开始加载资源!");
        _assetHandle = YooAssets.LoadAssetSync<GameObject>("Assets/Arts/ArtTestScenes/AbUnloadTest/Prefab/TestModel");
        model = _assetHandle.InstantiateSync(parentObj.transform);
        
    }

    
    
    public void DestroyModel()
    {
        Destroy(model);//销毁GameObject
        _assetHandle.Release();//引用计数-1
        var package = YooAssets.GetPackage("DefaultPackage");
        package.TryUnloadUnusedAsset("Assets/Arts/ArtTestScenes/AbUnloadTest/Prefab/TestModel");//释放GameObject依赖的Asset
    }
}
相关推荐
小南家的青蛙1 小时前
O3DE社区发布2510.1版本
游戏引擎·图形引擎
示申○言舌1 小时前
Unity高性能参数差异化URP Shader圆角圆环UI进度条
ui·unity·游戏引擎·圆环进度条·参数差异化·材质参数独立·圆角圆环
一只一只13 小时前
Unity之协程
unity·游戏引擎·协程·coroutine·startcoroutine
NIKITAshao1 天前
Unity 跨项目稳定迁移资源
unity·游戏引擎
CreasyChan1 天前
Unity FairyGUI高斯模糊实现方法
unity·游戏引擎·fgui
avi91111 天前
Unity半官方的AssetBundleBrowser插件说明+修复+Reporter插件
unity·游戏引擎·打包·assetbundle·游戏资源
郝学胜-神的一滴1 天前
深入理解Mipmap:原理、实现与应用
c++·程序人生·unity·游戏程序·图形渲染·unreal engine
一个笔记本1 天前
godot log | 修改main scene
游戏引擎·godot
nnsix2 天前
Unity PicoVR开发 实时预览Unity场景 在Pico设备中(串流)
unity·游戏引擎