1. 场景切换时“凭空消失”的对象:这不是Bug,是Unity的内存管理逻辑在说话
你有没有遇到过这样的情况:在Unity里写好了一个全局音效管理器,挂载在空GameObject上,脚本里用 DontDestroyOnLoad(this.gameObject) 做了标记;可一到第二个场景加载完成,调用 AudioManager.Instance.Play("bgm") 就直接抛NullReferenceException?打开Hierarchy窗口一看,那个挂着AudioManager脚本的GameObject确实不见了——不是被Destroy了,是压根没进新场景。更诡异的是,有时候它又“活”着,有时候却像被系统悄悄回收了一样。这不是玄学,也不是Unity抽风,而是绝大多数人根本没搞懂 DontDestroyOnLoad 的生效边界、触发时机和底层约束条件。
这个标题里的关键词—— Unity场景切换、对象销毁、DontDestroyOnLoad、持久化单例 ——其实指向一个非常具体、高频、且极易被误读的工程实践陷阱。它不涉及Shader编写或物理模拟,但几乎每个中型以上项目都会踩坑;它看起来只是加一行代码的事,实则牵扯到Unity的场景生命周期管理、对象引用追踪、GC回收策略、甚至跨场景资源依赖图的构建逻辑。我带过的三个项目组,平均每个组在第一个月都会为这个问题开两次紧急会议,有人重写整套事件系统,有人强行用ScriptableObject绕道,还有人干脆放弃单例改用静态字段——结果在热更新或多线程环境下全崩了。
这篇文章不是讲“怎么写单例”,而是聚焦于 为什么 DontDestroyOnLoad 在场景切换时会失效、什么时候会失效、以及如何让它的行为完全可控 。你会看到:它到底把谁“钉”在了内存里?为什么 DontDestroyOnLoad(gameObject) 和 DontDestroyOnLoad(component) 效果完全不同?为什么有些对象明明没被Destroy,却在新场景里查不到引用?更重要的是,我会带你从Unity Editor日志、Profiler内存快照、甚至IL反编译层面,还原 DontDestroyOnLoad 的真实执行链路。如果你正在做游戏主菜单→关卡→结算→再进关卡这类标准流程,或者需要跨场景保持登录态、背包数据、网络连接等核心状态,那这篇内容就是你上线前必须核对的 checklist。
2. DontDestroyOnLoad不是“永生符”,而是一张有严格准入规则的“临时居住证”
很多人第一次用 DontDestroyOnLoad 时,下意识把它理解成“把这个对象永久留在内存里”。这是最危险的认知偏差。Unity官方文档里那句“Marks the object as not destroyed automatically when loading a new scene”(标记对象在加载新场景时不被自动销毁),关键在“ not destroyed automatically ”——它只承诺不走自动销毁流程,但绝不保证该对象能被你随时访问、不被其他机制干预、甚至不被Unity内部逻辑主动清理。
2.1 它真正作用的对象,只有三种合法身份
DontDestroyOnLoad 的参数类型是 Object ,但实际能安全传入的,只有以下三类:
- Scene GameObject :在当前场景中实例化、且未被
Destroy()过的GameObject(注意:必须是场景根对象,不能是子物体); - Component attached to Scene GameObject :挂载在上述GameObject上的MonoBehaviour或ScriptableObject组件(但ScriptableObject需额外注意加载方式);
- Asset Object :已存在于Project窗口中的预制体(Prefab)、ScriptableObject资产(Asset),但 仅限Editor模式下有效,运行时传入会静默失败 。
提示:
DontDestroyOnLoad(transform)是非法操作,会直接报错;DontDestroyOnLoad(GetComponent<SomeScript>())看似可行,但若该脚本挂载在子物体上,父物体被销毁时子物体仍会连带消失——DontDestroyOnLoad只保护传入对象本身,不递归保护其父子关系树。
我曾在一个AR项目里栽过跟头:为了保持摄像头姿态跟踪器持续工作,我把 TrackingManager 脚本挂在一个空GameObject下,并在 Awake() 里调用 DontDestroyOnLoad(this.gameObject) 。测试时一切正常。但上线后用户反馈“切到设置页再返回,AR画面就黑了”。排查发现,设置页场景里有一个同名空GameObject,Unity在加载时执行了 SceneManager.SetActiveScene() ,触发了旧场景所有GameObject的 OnDisable() ,而 DontDestroyOnLoad 此时并未阻止该对象被标记为“非活动”,导致后续 FindObjectOfType<TrackingManager>() 返回null——因为 DontDestroyOnLoad 只管“不销毁”,不管“是否激活”。
2.2 生命周期断点:它只在SceneManager.LoadScene()链条中起效
DontDestroyOnLoad 的生效时机极其苛刻:它 仅在 SceneManager.LoadScene() 或 SceneManager.LoadSceneAsync() 开始执行、且旧场景尚未卸载(UnloadScene)之前 被调用才有效。一旦旧场景进入 UnloadScene 阶段,再调用 DontDestroyOnLoad 就完全无效。
我们来拆解一次标准场景切换的完整生命周期(以 SceneManager.LoadScene("Level1", LoadSceneMode.Single) 为例):
| 阶段 | 触发时机 | 关键行为 | DontDestroyOnLoad 是否有效 |
|---|---|---|---|
| 1. Pre-Load | LoadScene 调用后,新场景资源开始加载前 |
所有当前活动场景的 MonoBehaviour.OnApplicationPause(false) 、 OnDisable() 按顺序触发 |
✅ 有效(最后的安全窗口) |
| 2. Scene Swap | 新场景加载完成,旧场景开始卸载 | Unity内部执行 DestroyImmediate() 清理旧场景根GameObject |
❌ 失效(此时调用无任何反应) |
| 3. Post-Swap | 旧场景完全卸载,新场景 Awake() / Start() 开始执行 |
DontDestroyOnLoad 对象被移入特殊“Dont |

297

被折叠的 条评论
为什么被折叠?



