解释DontDestroyOnLoad对象在场景切换时的内存归属

解读

面试官抛出此题,核心想验证两件事:

  1. 候选人是否真正理解Unity场景卸载(SceneManager.UnloadScene/LoadScene)内存生命周期的边界;
  2. 能否把“对象留在内存”这一表象,拆解成托管堆、Native-Object、资源(Texture/Mesh/Shader)三层,并指出谁持有、谁释放、何时释放
    国内项目常因“DDOL(DontDestroyOnLoad)滥用”导致切换场景后内存峰值居高不下,面试官希望听到你量化风险(如Profiler中“Not Saved”内存曲线)并给出工程级治理方案,而非背诵官方文档。

知识点

  1. DDOL本质:Unity在Native层把目标GameObject从当前场景的Scene Root移到EngineInternalScene“DontDestroyOnLoad”(一个隐藏且永不卸载的场景)。
  2. 内存归属
    • 托管层(C#):Mono/IL2CPP 托管堆上的对象引用计数不变,GC Root 仍标记为有效,不会随原场景卸载被回收。
    • Native层(C++):Object* 指针被引擎的 PersistentManager 持有,不受场景卸载影响
    • 资源层(Assets):若 DDOL 对象身上挂有引用纹理、网格、Shader的字段,则这些资源会被ResourceManager 的 Persistent 队列强引用,常驻内存,直到 DDOL 对象主动销毁或调用 Resources.UnloadUnusedAssets。
  3. 生命周期边界:只有显式 Destroy() 或**切换整个游戏会话(进程退出)**才会释放;SceneManager.UnloadScene/LoadScene对 DDOL 无效。
  4. 中国手游常见坑
    • 整棵UI Root设为 DDOL,导致图集、字体、音效全部常驻,低端机 1~2 轮场景切换后PSS上涨 200+ MB,被腾讯 WeTest 性能测试直接打回。
    • 热更新框架(HybridCLR/ILRuntime)把热更DLL的静态容器挂到 DDOL,结果卸载 Assembly 后托管委托仍指向已释放内存,触发闪退。

答案

“DDOL 对象在场景切换时,其内存归属从原场景迁移到引擎内部的持久化场景 DontDestroyOnLoad,因此:

  1. 托管堆上的实例不会被 GC,因为 PersistentManager 维持 Native 引用,GC Root 仍有效;
  2. Native Object 本身由引擎的 PersistentManager 持有,生命周期与进程一致
  3. 引用的资源(贴图、网格、Shader)因对象仍在资源管理器的持久引用列表不会随场景卸载被卸载,必须手动调用 Destroy(obj) 或 Resources.UnloadUnusedAssets 才能释放。

简言之,DDOL 让对象脱离场景卸载的管辖,进入‘进程级’内存池,若内部挂有大资源,需由开发者显式销毁弱引用+缓存池治理,否则会造成PSS 常驻峰值,在国内安卓渠道 4G 内存机型上极易被杀后台。”

拓展思考

  1. 量化治理
    • 在 Profile 模式下使用Memory Profiler 的“Snapshot Diff”,筛选“Not Saved”标签,定位 DDOL 带来的常驻纹理/网格大小
    • 制定“DDOL 白名单”规范,只允许单例管理器(Sound、Network、SDK)进入持久场景,UI 面板、模型预制体一律使用对象池+弱引用,确保切换场景后PSS 回降率 ≥ 90%
  2. 多场景并行加载(Addressables)
    • 若项目使用Addressables.LoadSceneAsync(SceneLoadMode.Additive),DDOL 对象仍归 DontDestroyOnLoad 场景,不会被任何 Additive 场景卸载;
    • 但 Addressables 的AssetReference 计数会因 DDOL 强引用无法归零,导致重复加载同一标签资源时出现“双份内存”,需在管理器里手动 Release 并改用WeakAssetReference包装。
  3. IL2CPP 陷阱
    • 在 IL2CPP 构建下,泛型静态字段若被 DDOL 对象引用,AOT 元数据也会常驻,无法通过 Resources.UnloadUnusedAssets 卸载
    • 解决方法是把泛型容器拆成非泛型基类清空静态字段后再销毁 DDOL,避免代码段内存无端膨胀 5~10 MB。