做过一段时间 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(); // 异步等待引擎资源回收完成
特点:
-
类似"引擎层的垃圾回收":
-
Unity 找出所有仍被引用的 Asset
-
对于没有引用的 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
}
}