在Assembly Reload阶段如何保存非序列化状态
解读
Unity 在脚本编译完成后会触发 Assembly Reload,此时所有非序列化字段(如 System.Action、网络连接、线程句柄、缓存字典等)都会被 强制清零。国内项目普遍使用 ILRuntime/hybridclr 热更 或 大量运行时缓存,一旦状态丢失,热更后玩家会直接掉线、UI 数据对不上,甚至引发 Sentry 上报异常。面试官想确认两点:
- 你是否真的理解 Assembly Reload 的触发时机(编译脚本、切换平台、导入插件、进入 PlayMode 的 Reload Domain 选项);
- 能否给出 零 GC、可扩展、可回滚 的实战方案,而不是简单写个
ScriptableObject就完事。
知识点
- ReloadDomain 与 Reload Scene 开关(Unity 2019.3+,国内团队常关前者以提速,但 Domain Reload 关闭后静态字段不会清空,反而更容易踩坑)。
- Unity 序列化规则:public 或
[SerializeField]字段才会被 C++ 端内存镜像 保存;Dictionary、delegate、event、Task、Socket、CancellationTokenSource 等 托管堆对象 一律不序列化。 - ScriptableObject 生命周期:它不受 Reload 影响,但只能保存 可序列化 的数据;非序列化状态需要手动转储。
- ISerializationCallbackReceiver 与
[OnSerializing]/[OnDeserialized]:在 Assembly Reload 前后由 Unity 主动调用,是 官方推荐的转储钩子。 - 内存快照 vs 增量回滚:国内大型项目(如 SLG、开放世界)会自定义 Delta State Machine,把非序列化对象映射到 值类型副本 或 BlobAsset,Reload 后再 延迟重建,避免高频 GC。
- EditorOnly 与 Player 差异:Editor 下可以反射访问 UnityEditorInternal.InternalEditorUtility 的
SaveToSerializedFileAndForget做临时快照;Player 下只能依赖 自定义存档系统 或 网络回包重放。
答案
核心思路:“把非序列化状态转成可序列化描述,Reload 后再按需重建”。
步骤如下:
- 定义 只含值类型的 DTO(Data Transfer Object),把 Dictionary、delegate 等拆成 List<SerializablePair>
或int hash + string methodName`。 - 在 主业务单例 中实现
ISerializationCallbackReceiver:OnBeforeSerialize():把当前非序列化字段写入 DTO,并塞进 ScriptableObject 容器(容器标记[CreateAssetMenu]并放Assets/RuntimeData,打包时排除)。OnAfterDeserialize():从容器读出 DTO,延迟一帧 后在RuntimeInitializeLoadType.AfterSceneLoad阶段重建原始对象;重建失败时走 默认降级逻辑,保证玩家无感知。
- 对 网络/线程资源(Socket、CancellationTokenSource)不直接保存,而是记录 sessionId + reconnectToken,Reload 后触发 断线重连 并 回滚 RPC 队列,与后端对齐。
- 在 ILRuntime/hybridclr 热更层 同样实现一套
CrossDomainSerializer,把热更里的Delegate转成 MethodToken + Target 索引,Reload 后通过 缓存接口表 重新绑定,避免MissingReferenceException。 - 编辑器下开启 Enter PlayMode Options 关闭 Domain Reload 时,一定在
StaticConstructor里做 手动清零检查,防止静态字段脏数据;同时用[InitializeOnLoadMethod]注册 DomainReload 事件,把容器持久化到 Temp/AssemblyReloadCache.asset,退出 PlayMode 自动删除,避免污染版本库。
一句话总结:“用 ScriptableObject 做中转,DTO 做描述,延迟重建做恢复,网络资源做回滚”,就能在 Assembly Reload 阶段 零丢失 地保存非序列化状态。
拓展思考
-
如果项目关闭 Domain Reload 且使用 Entities 1.0,如何把
SystemRef和EntityQuery也做快照?
提示:可借助EntityManager.GetChunk()拿到 ComponentTypeHash,Reload 后通过EntityQueryBuilder重建,并用EntityCommandBuffer回放修改。 -
在 WebGL 平台,线程和 Socket 不可用,Reload 后如何 秒级恢复长连接?
提示:使用 WebSocket 的子协议扩展,把 lastBinaryMessageId 存到SessionStorage,Reload 后先发送Recover(id)帧,服务端 重放增量帧,客户端 插值渲染,实现 无感知重连。