解释DontDestroyOnLoad对象在场景切换时的内存归属
解读
面试官抛出此题,核心想验证两件事:
- 候选人是否真正理解Unity场景卸载(SceneManager.UnloadScene/LoadScene)与内存生命周期的边界;
- 能否把“对象留在内存”这一表象,拆解成托管堆、Native-Object、资源(Texture/Mesh/Shader)三层,并指出谁持有、谁释放、何时释放。
国内项目常因“DDOL(DontDestroyOnLoad)滥用”导致切换场景后内存峰值居高不下,面试官希望听到你量化风险(如Profiler中“Not Saved”内存曲线)并给出工程级治理方案,而非背诵官方文档。
知识点
- DDOL本质:Unity在Native层把目标GameObject从当前场景的Scene Root移到EngineInternalScene“DontDestroyOnLoad”(一个隐藏且永不卸载的场景)。
- 内存归属:
- 托管层(C#):Mono/IL2CPP 托管堆上的对象引用计数不变,GC Root 仍标记为有效,不会随原场景卸载被回收。
- Native层(C++):Object* 指针被引擎的 PersistentManager 持有,不受场景卸载影响。
- 资源层(Assets):若 DDOL 对象身上挂有引用纹理、网格、Shader的字段,则这些资源会被ResourceManager 的 Persistent 队列强引用,常驻内存,直到 DDOL 对象主动销毁或调用 Resources.UnloadUnusedAssets。
- 生命周期边界:只有显式 Destroy() 或**切换整个游戏会话(进程退出)**才会释放;SceneManager.UnloadScene/LoadScene对 DDOL 无效。
- 中国手游常见坑:
- 把整棵UI Root设为 DDOL,导致图集、字体、音效全部常驻,低端机 1~2 轮场景切换后PSS上涨 200+ MB,被腾讯 WeTest 性能测试直接打回。
- 热更新框架(HybridCLR/ILRuntime)把热更DLL的静态容器挂到 DDOL,结果卸载 Assembly 后托管委托仍指向已释放内存,触发闪退。
答案
“DDOL 对象在场景切换时,其内存归属从原场景迁移到引擎内部的持久化场景 DontDestroyOnLoad,因此:
- 托管堆上的实例不会被 GC,因为 PersistentManager 维持 Native 引用,GC Root 仍有效;
- Native Object 本身由引擎的 PersistentManager 持有,生命周期与进程一致;
- 引用的资源(贴图、网格、Shader)因对象仍在资源管理器的持久引用列表,不会随场景卸载被卸载,必须手动调用 Destroy(obj) 或 Resources.UnloadUnusedAssets 才能释放。
简言之,DDOL 让对象脱离场景卸载的管辖,进入‘进程级’内存池,若内部挂有大资源,需由开发者显式销毁或弱引用+缓存池治理,否则会造成PSS 常驻峰值,在国内安卓渠道 4G 内存机型上极易被杀后台。”
拓展思考
- 量化治理:
- 在 Profile 模式下使用Memory Profiler 的“Snapshot Diff”,筛选“Not Saved”标签,定位 DDOL 带来的常驻纹理/网格大小;
- 制定“DDOL 白名单”规范,只允许单例管理器(Sound、Network、SDK)进入持久场景,UI 面板、模型预制体一律使用对象池+弱引用,确保切换场景后PSS 回降率 ≥ 90%。
- 多场景并行加载(Addressables):
- 若项目使用Addressables.LoadSceneAsync(SceneLoadMode.Additive),DDOL 对象仍归 DontDestroyOnLoad 场景,不会被任何 Additive 场景卸载;
- 但 Addressables 的AssetReference 计数会因 DDOL 强引用无法归零,导致重复加载同一标签资源时出现“双份内存”,需在管理器里手动 Release 并改用WeakAssetReference包装。
- IL2CPP 陷阱:
- 在 IL2CPP 构建下,泛型静态字段若被 DDOL 对象引用,AOT 元数据也会常驻,无法通过 Resources.UnloadUnusedAssets 卸载;
- 解决方法是把泛型容器拆成非泛型基类或清空静态字段后再销毁 DDOL,避免代码段内存无端膨胀 5~10 MB。