ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

为什么关掉域重载后 static 不会归零

为什么关掉域重载后 static 不会归零 核心原因一句话:static 变量不属于你的游戏,属于加载了你代码的那个运行环境。退出 Play 并不会关掉这个环境——关掉域重载,就是不让 Unity 重建它。一、先搞清 static 存在哪普通字段和 static 字段的存储位置完全不同。你 new 一个 Player 对象 ↓ 在堆上分配一块内存,存实例字段 ↓ 对象被销毁 / GC 回收 → 这块内存没了 ✅ static 字段 ↓ 不在任何对象里,而是存在「这个类型本身」的一块内存里 ↓ 这块内存属于:加载了这个类型的程序集 ↑ 程序集属于:.NET 的「域」(AppDomain)┌─ Unity Editor 进程 ─────────────────────────────┐ │ │ │ ┌─ 脚本域 (Scripting Domain) ────────────────┐ │ │ │ │ │ │ │ Assembly-CSharp.dll │ │ │ │ ├─ 类型 GameManager │ │ │ │ │ └─ static Instance ← 值存在这里 │ │ │ │ │ └─ static _score ← 值存在这里 │ │ │ │ └─ 类型 Player │ │ │ │ │ │ │ │ UnityEditor.dll │ │ │ │ ... │ │ │ └────────────────────────────────────────────┘ │ │ │ │ 场景里的 GameObject、组件实例 │ │ ← 退出 Play 时这些被销毁 │ └─────────────────────────────────────────────────┘退出 Play 模式,Unity 销毁的是场景里的对象。它没有理由去动域那一层的东西——那一层还装着整个编辑器本身。二、域重载开着的时候,发生了什么点 Play │ ├─ Unity 把整个脚本域扔掉 │ → 所有程序集卸载 │ → 所有类型的 static 存储区一起消失 │ ├─ 重新加载程序集 │ → 类型重新初始化 │ → static 字段回到初始值(0 / null / 字段初始化器的值) │ └─ 开始运行 ✅ 一切从零开始这就是域重载顺便帮你清理 static 的原理——它不是在重置变量,它是把装变量的整个容器换了一个新的。开着域重载: 第一次 Play → 域 A → _score 0 → 玩到 500 退出 第二次 Play → 域 B ← 全新的容器 → _score 0 ✅ (域 A 连着里面的 500 一起被扔掉了) 关掉域重载: 第一次 Play → 域 A → _score 0 → 玩到 500 退出 ← 域 A 还在,500 还在 第二次 Play → 域 A ← 同一个容器 → _score 500 ❌三、为什么开着域重载这么慢因为它要扔掉的不只是你的代码:卸载旧域 重新加载 Assembly-CSharp.dll 重新加载所有第三方插件程序集 重新加载 UnityEngine / UnityEditor 相关程序集 重建所有反射缓存(序列化、Inspector、菜单项) 重新执行所有 [InitializeOnLoad] 和静态构造 重建 C# ↔ C 的类型绑定代码越多、插件越多,这个过程越久。这也解释了为什么项目早期点 Play 很快,后期要等十几秒。┌────────────────────────────────────────────────┐ │ 域重载做的事 模拟「进程重新启动」 │ │ │ │ 为什么要模拟?因为真机上就是这样: │ │ 玩家点开游戏 → 新进程 → static 必然是初始值 │ │ │ │ 所以域重载是在帮你保证「编辑器行为 真机行为」 │ └────────────────────────────────────────────────┘关掉它,本质上是用编辑器和真机行为不一致的风险,换迭代速度。所以风险要你自己管。四、一个关键细节:字段初始化器只跑一次很多人以为写了初始值就安全了:publicclassScoreManager{privatestaticint_score0;// 以为每次 Play 都会执行?privatestaticListItem_itemsnewListItem();}这两行代码不属于每次 Play 执行,而属于类型第一次被用到时执行一次。C# 会把它们塞进一个隐式的静态构造函数:// 编译器实际生成的东西(概念上)staticScoreManager(){_score0;_itemsnewListItem();}静态构造函数的触发规则是:在一个域的生命周期里,每个类型最多执行一次关掉域重载后: 第一次 Play 首次访问 ScoreManager → 静态构造执行 → _score 0, _items 空列表 ✅ 游戏跑完 → _score 500, _items 有 30 个东西 退出 Play(域没变,类型已初始化过) 第二次 Play 访问 ScoreManager → 静态构造「已经执行过了」,不会再执行 ❌ → _score 还是 500,_items 还是那 30 个这是最反直觉的一点:你写了 0,但它不会再跑第二次。五、哪些会出事,哪些不会┌─ 退出 Play 时会自动清掉 ─────────────────────┐ │ 场景里的 GameObject 和组件 │ │ 实例字段(跟着对象一起没了) │ │ Coroutine(宿主对象销毁后停止) │ │ Unity 的 Update/FixedUpdate 调用 │ └───────────────────────────────────────────────┘ ┌─ 关掉域重载后会残留 ─────────────────────────┐ │ static 字段 / 属性 ← 最常见 │ │ static event ← 最危险 │ │ static 集合(List / Dictionary / 对象池) │ │ 单例的 Instance 引用 │ │ static 构造里建立的缓存 │ │ 自己起的线程、未取消的 Task │ │ 注册到 Application 的回调 │ │ 第三方插件内部的 static 状态 ← 你改不了 │ └───────────────────────────────────────────────┘最危险的是 static eventpublicstaticeventActionintOnScoreChanged;第一次 Play HudController 实例 A 订阅 → 订阅列表: [A] 退出 Play A 这个 GameObject 被销毁了 但订阅列表里还挂着 A 的引用 ← 没人清它 第二次 Play HudController 实例 B 订阅 → 订阅列表: [A(已销毁), B] 触发事件 → 调到 A 的方法 → A 内部访问自己的 transform → MissingReferenceException症状非常典型:第一次 Play 正常,第二次才报错;重启 Unity 又正常了。看到这个组合就去查 static event。而且这同时是内存泄漏:每次 Play 都往列表里加一个永远不会被回收的对象。六、怎么正确处理用[RuntimeInitializeOnLoadMethod],它的调用时机是每次进入 Play,与域重载开关无关。usingSystem;usingSystem.Collections.Generic;usingUnityEngine;publicclassScoreManager:MonoBehaviour{publicstaticScoreManagerInstance;publicstaticeventActionintOnScoreChanged;privatestaticint_score;privatestaticreadonlyListItem_itemsnewListItem();// ★ 每次进入 Play 都会执行,这才是可靠的重置点[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]privatestaticvoidResetStatics(){Instancenull;OnScoreChangednull;// 清空订阅者,这一行最重要_score0;_items.Clear();// readonly 集合不能重新赋值,用 Clear}voidAwake()Instancethis;}为什么选SubsystemRegistrationRuntimeInitializeLoadType 的执行顺序: SubsystemRegistration ← 最早,Unity 子系统注册阶段。重置 static 用这个 AfterAssembliesLoaded BeforeSplashScreen BeforeSceneLoad ← 场景加载前 AfterSceneLoad ← 场景加载后,Awake 已经跑过了 ❌ 太晚必须早于任何Awake,否则你可能先被某个Awake写入了值,然后又被重置覆盖掉。另一种写法:干脆别用可变 static// ❌ 可变 static,需要手动维护重置publicstaticGameManagerInstance;// ✅ 挂在场景里的单例,退出 Play 自动随对象销毁publicclassGameManager:MonoBehaviour{// 不存 static 引用,需要时查找或靠依赖注入传入}// ✅ 这些 static 完全安全,不需要重置publicconstintMaxPlayers4;// constpublicstaticreadonlystringVersion1.0;// readonly 且值不变┌───────────────────────────────────────────────────┐ │ const → 安全,编译期常量 │ │ static readonly → 值本身安全(但如果是集合, │ │ 内容仍会残留,要 Clear) │ │ static 可变字段 → 必须手动重置 │ │ static event → 必须手动置 null,优先级最高 │ └───────────────────────────────────────────────────┘注意中间那条:static readonly ListT的引用不会变,但内容会残留。这个很容易漏。七、怎么判断自己中招了症状对照:现象大概率是第二次 Play 才出 bug,第一次正常static 残留重启 Unity 就好了static 残留(重启换域)分数/进度带着上次的值static 字段没重置MissingReferenceException,指向已销毁对象static event 没清对象池里混进了上次的对象static 集合没 Clear事件被触发了两次、三次每次 Play 都重复订阅同一个 static event真机正常但编辑器反复测试出问题典型的域重载差异快速验证法:把 Reload Domain 重新勾上 → 问题消失 → 100% 确认是 static 残留问题 → 再关掉,去找那些 static八、归纳1. static 的存储位置在「域」这一层,不在场景、不在对象里 2. 退出 Play 只销毁场景对象,碰不到域 → 域里的 static 自然留着 3. 域重载的清理效果是「换了个新容器」的副作用 不是它主动在重置变量 4. 字段初始化器 静态构造 一个域里只跑一次 所以写了 0 也不管用 5. 唯一可靠的重置点: [RuntimeInitializeOnLoadMethod(SubsystemRegistration)] 6. static event 必须置 null —— 它既是 bug 源也是内存泄漏源 7. 关掉域重载 用「编辑器与真机行为不一致」的风险换速度 风险的管理责任转移到你身上顺带一提:第三方插件内部的 static 你管不了。如果关掉域重载后某个插件行为异常,又找不到原因,就在 Project Settings 里保留域重载、只关 Scene Reload——后者也能省不少时间,且几乎没有副作用。
返回列表