如何在子场景卸载时自动释放其Addressable标签资源
解读
国内项目普遍采用“子场景(Sub-Scene)+ Addressable”做大世界分块或副本动态加载,目的是降低首包大小、缩短进入游戏时间。
面试官真正想确认的是:
- 你是否理解 Addressable 的引用计数与子场景生命周期是两条独立管线;
- 能否给出零泄漏、零卡顿、可维护的自动化方案,而不是让策划或美术手动填 Release 路径;
- 是否具备线上容错意识——一旦 Release 失败或异步操作被打断,如何兜底。
知识点
- Addressable 标签(Label) 本身不是资源,只是查询 key;真正的引用计数挂在 LocationHandle 上。
- SceneInstance 被 Unload 时,Unity 只卸载 Scene 对象,不会自动 Release Addressable 依赖;必须手动调用 Addressables.Release(handle)。
- 子场景在 Addressable 里通常打成一个 AssetBundle,其依赖项(贴图、Mesh、Shader 等)会被隐式引用;只要主 Bundle 的引用计数不归零,依赖 Bundle 也不会被卸载。
- 国内主流热更框架(如 GameFramework、QFramework、Luban)都会封装一层 ResourceHolder,记录 handle→scene 的映射,防止重复加载或误释放。
- 2021 LTS 之后,Addressable 提供 ResourceLocationMap.ChainOperation 接口,可以异步批量查询标签对应的所有 Location,避免同步遍历卡顿。
- 释放时必须 先卸载子场景,再 Release handle,否则会因为 Unity 对象被销毁而触发 InvalidOperationException;若顺序反了,Profiler 里会看到 “Release while scene is still loaded” 警告。
- 对于弱联网项目,还要考虑玩家断线重连:子场景卸载后若玩家重新进入,必须能无感知重新加载,因此释放前要把场景状态快照写到本地缓存,不能简单粗暴地清掉所有引用。
答案
-
统一封装 SubSceneManager,内部维护
Dictionary<string, SceneInstance> loadedSubScenes;
Dictionary<string, AsyncOperationHandle> sceneHandleMap;
Key 统一用 子场景 Address 的 PrimaryKey(通常是“Assets/Scenes/Dungeon_01.unity”)。 -
加载时
var handle = Addressables.LoadSceneAsync(sceneKey, LoadSceneMode.Additive, false); await handle.Task; loadedSubScenes[sceneKey] = handle.Result; sceneHandleMap[sceneKey] = handle; // 记录 handle,用于后续释放 -
卸载时
public async UniTask UnloadSubScene(string sceneKey) { if (!loadedSubScenes.TryGetValue(sceneKey, out var sceneInstance)) return; // 1. 先卸载场景 await Addressables.UnloadSceneAsync(sceneInstance, true).ToUniTask(); // 2. 再释放 Addressable 引用 if (sceneHandleMap.TryGetValue(sceneKey, out var handle)) { Addressables.Release(handle); sceneHandleMap.Remove(sceneKey); } loadedSubScenes.Remove(sceneKey); }注意:Release 必须在 UnloadSceneAsync 完成之后,否则会出现“Trying to release a scene that is still loaded”警告。
-
若需按标签批量释放(例如“Dungeon”标签下所有子场景),则
var locations = await Addressables.LoadResourceLocationsAsync("Dungeon").Task; foreach (var loc in locations) { if (sceneHandleMap.TryGetValue(loc.PrimaryKey, out var h)) { await UnloadSubScene(loc.PrimaryKey); } } -
兜底策略
- 在 Application.lowMemory 回调里,遍历 sceneHandleMap,按**最近最少使用(LRU)**顺序释放,防止低端机 OOM;
- 释放失败时捕获 InvalidKeyException,写本地日志并上报 Sentry,方便线上复盘;
- 使用 Addressables.ReleaseInstance 而非 Release,如果将来把子场景改成 Prefab 动态实例化 也能复用同一套代码。
拓展思考
- 分帧释放:当子场景资源量巨大(>200 MB)时,一次性 Release 会造成 GPU 卡顿,可把 handle 拆成 N 批,每帧释放 20%,用 TimeSlicer 调度。
- 引用计数增强:在 handle 外层再包一层 WeakReference,配合 Custom Allocation Profiler 模块,可在 Editor 下可视化查看哪个子场景泄漏,解决国内项目“策划反复进退副本导致内存爆炸” 的老大难问题。
- 与 ILRuntime 热更兼容:若脚本端用 ILRuntime,跨域继承的 MonoBehaviour 会被 Hook 到 Addressable 的 ResourceLocator 中,卸载子场景前必须先 调用 ILRuntime 的 CrossDomain Clean,否则会出现“Can't destroy ScriptableObject”的 Fatal 错误。
- WebGL 特例:WebGL 平台没有文件系统缓存,Addressable 的 Bundle 卸载后再次加载要走 远程 CDN,必须提前把高频子场景 Bundle 打进 首包 StreamingAssets,避免玩家“进退副本”时反复下载;此时释放策略改为只减引用计数不真正卸载,等玩家退回登录界面再统一 Caching.ClearCache()。