Lesson 4 / 6
生命周期与序列化安全
Unity 新人最常见的坑:对象会启用、禁用、销毁;字段名和 Inspector 引用会被序列化保存。
生命周期最小集
页面从出现到消失
Awake对象加载
OnEnable订阅 / 启动
Start首次刷新
Update每帧,高频
OnDisable取消 / 停止
OnDestroy销毁
异步请求可能跨过 OnDisable / OnDestroy 返回。验收必须覆盖“关闭页面后不再改 UI”。
| 方法 | 什么时候发生 | 适合做什么 |
|---|---|---|
Awake | 对象被加载时 | 初始化内部状态,获取自身组件。 |
OnEnable | 对象每次启用 | 订阅事件、注册回调、启动临时任务。 |
Start | 第一次 Update 前 | 依赖其他对象已初始化后的首刷。 |
Update | 每帧调用 | 极少量实时逻辑;不要做网络请求、IO、大量查找。 |
OnDisable | 对象每次禁用 | 取消订阅、取消请求、停止临时任务。 |
OnDestroy | 对象销毁 | 释放资源,兜底清理。 |
后台迁移提醒: 不要把 Unity 页面当成长生不死的 singleton。用户可以关闭页面,Scene 可以切换,对象可以被对象池复用。
HappyMahjong 中的生命周期锚点
Assets/Scripts/Boot/LoadDll.cs:Awake()/Start()是启动流程入口。Assets/Scripts/InGameScripts/InGameScripts/Hall/AppMain/AppRoot.cs:Start()中创建context = new AppContext(this)。Assets/Scripts/InGameScripts/InGameScripts/Hall/Login/LoginRoot.cs:Awake()中创建context = new LoginContext(this),OnDestroy()上报离开登录。*Context.cs的绑定不是普通初始化代码;它决定 View / Mediator / Command / Model / Service 是否能被框架找到。
事件订阅必须成对
private void OnEnable()
{
_service.Changed += HandleChanged;
}
private void OnDisable()
{
_service.Changed -= HandleChanged;
}
如果只订阅不取消,第二次打开页面可能重复回调;页面关闭后也可能访问已经销毁的 UI。
异步必须考虑页面关闭
private CancellationTokenSource _loadCts;
private void OnEnable()
{
_loadCts = new CancellationTokenSource();
_ = LoadAsync(_loadCts.Token);
}
private void OnDisable()
{
_loadCts?.Cancel();
_loadCts?.Dispose();
_loadCts = null;
}
不要求产品同学会写这段代码,但要会把它写进验收:请求中关闭页面,不应再弹错误或改 UI。HappyMahjong 里还要关注 TSDK 回调、Command dispatch 和弹窗关闭后的残留回调。
Inspector 序列化风险
代码字段和 Inspector 引用是绑在一起的
C# 字段名
loginButton↔
Inspector 引用
Scene / Prefab 中保存的对象引用
Scene / Prefab 中保存的对象引用
字段重命名不是纯代码重构。旧序列化数据可能找不到新字段名。
Unity 可以把字段保存到 Scene / Prefab / Asset 中:
[SerializeField] private Button loginButton;
[SerializeField] private TMP_InputField usernameInput;
// HappyMahjong 中也可能有点击接收对象或弹窗引用:
[SerializeField] private GameObject emptyClick;
[SerializeField] private GameObject rewardPopup;
如果 agent 把字段名从 loginButton 改成 submitButton,旧 Inspector 绑定可能丢失。安全做法是检查是否需要:
using UnityEngine.Serialization;
[FormerlySerializedAs("loginButton")]
[SerializeField] private Button submitButton;
课堂练习:找出两个风险
private async void LoadUser()
{
var user = await _service.LoadUserAsync();
nameText.text = user.Name;
}
期望答案:
- 没有取消:await 返回时页面可能已经关闭。
- 可能访问已销毁对象:
nameText所属 GameObject 可能不存在。 async void异常处理困难;应沿用项目已有异步方案。
Retrieval check
重命名 `[SerializeField]` 字段前,最该检查什么?
Primary source
课后阅读 Unity 官方 MonoBehaviour 和 serialization 文档。项目内优先读 Docs/01-flows/startup-flow.md、Docs/01-flows/login-flow.md。课堂只要求记住两件事:生命周期会反复发生,Inspector 引用和 StrangeIoC 绑定都是项目状态的一部分。
Ask the agent: “请专门检查这次修改是否有生命周期、异步取消、对象销毁后访问、序列化字段兼容性和 StrangeIoC binding 风险。”