在Assembly Reload阶段如何保存非序列化状态

解读

Unity 在脚本编译完成后会触发 Assembly Reload,此时所有非序列化字段(如 System.Action、网络连接、线程句柄、缓存字典等)都会被 强制清零。国内项目普遍使用 ILRuntime/hybridclr 热更大量运行时缓存,一旦状态丢失,热更后玩家会直接掉线、UI 数据对不上,甚至引发 Sentry 上报异常。面试官想确认两点:

  1. 你是否真的理解 Assembly Reload 的触发时机(编译脚本、切换平台、导入插件、进入 PlayMode 的 Reload Domain 选项);
  2. 能否给出 零 GC、可扩展、可回滚 的实战方案,而不是简单写个 ScriptableObject 就完事。

知识点

  1. ReloadDomainReload Scene 开关(Unity 2019.3+,国内团队常关前者以提速,但 Domain Reload 关闭后静态字段不会清空,反而更容易踩坑)。
  2. Unity 序列化规则:public 或 [SerializeField] 字段才会被 C++ 端内存镜像 保存;Dictionary、delegate、event、Task、Socket、CancellationTokenSource 等 托管堆对象 一律不序列化。
  3. ScriptableObject 生命周期:它不受 Reload 影响,但只能保存 可序列化 的数据;非序列化状态需要手动转储。
  4. ISerializationCallbackReceiver[OnSerializing]/[OnDeserialized]:在 Assembly Reload 前后由 Unity 主动调用,是 官方推荐的转储钩子
  5. 内存快照 vs 增量回滚:国内大型项目(如 SLG、开放世界)会自定义 Delta State Machine,把非序列化对象映射到 值类型副本BlobAsset,Reload 后再 延迟重建,避免高频 GC。
  6. EditorOnly 与 Player 差异:Editor 下可以反射访问 UnityEditorInternal.InternalEditorUtilitySaveToSerializedFileAndForget 做临时快照;Player 下只能依赖 自定义存档系统网络回包重放

答案

核心思路:“把非序列化状态转成可序列化描述,Reload 后再按需重建”
步骤如下:

  1. 定义 只含值类型的 DTO(Data Transfer Object),把 Dictionary、delegate 等拆成 List<SerializablePair>int hash + string methodName`
  2. 主业务单例 中实现 ISerializationCallbackReceiver
    • OnBeforeSerialize():把当前非序列化字段写入 DTO,并塞进 ScriptableObject 容器(容器标记 [CreateAssetMenu] 并放 Assets/RuntimeData打包时排除)。
    • OnAfterDeserialize():从容器读出 DTO,延迟一帧 后在 RuntimeInitializeLoadType.AfterSceneLoad 阶段重建原始对象;重建失败时走 默认降级逻辑,保证玩家无感知。
  3. 网络/线程资源(Socket、CancellationTokenSource)不直接保存,而是记录 sessionId + reconnectToken,Reload 后触发 断线重连回滚 RPC 队列,与后端对齐。
  4. ILRuntime/hybridclr 热更层 同样实现一套 CrossDomainSerializer,把热更里的 Delegate 转成 MethodToken + Target 索引,Reload 后通过 缓存接口表 重新绑定,避免 MissingReferenceException
  5. 编辑器下开启 Enter PlayMode Options 关闭 Domain Reload 时,一定在 StaticConstructor 里做 手动清零检查,防止静态字段脏数据;同时用 [InitializeOnLoadMethod] 注册 DomainReload 事件,把容器持久化到 Temp/AssemblyReloadCache.asset退出 PlayMode 自动删除,避免污染版本库。

一句话总结:“用 ScriptableObject 做中转,DTO 做描述,延迟重建做恢复,网络资源做回滚”,就能在 Assembly Reload 阶段 零丢失 地保存非序列化状态。

拓展思考

  1. 如果项目关闭 Domain Reload 且使用 Entities 1.0,如何把 SystemRefEntityQuery 也做快照?
    提示:可借助 EntityManager.GetChunk() 拿到 ComponentTypeHash,Reload 后通过 EntityQueryBuilder 重建,并用 EntityCommandBuffer 回放修改。

  2. WebGL 平台,线程和 Socket 不可用,Reload 后如何 秒级恢复长连接
    提示:使用 WebSocket 的子协议扩展,把 lastBinaryMessageId 存到 SessionStorage,Reload 后先发送 Recover(id) 帧,服务端 重放增量帧,客户端 插值渲染,实现 无感知重连