UI 生命周期机制
本章节专门介绍 Fink Framework 中 UI 面板的 完整生命周期体系。
所有 UI 面板均继承自 BasePanel,并自动遵循框架统一的生命周期流程。
生命周期设计的目标是:
- 明确 首次创建、每次显示、隐藏、销毁 的逻辑分工
- 避免开发者手动判断 UI 状态
- 自动处理 UI 复用、异步加载、回调等复杂行为
- 防止事件未解绑导致的内存泄漏
1. 生命周期总览
一个 UI 面板完整的生命周期如下:
Unity Awake (自动绑定) → ShowMe() → OnShow()
↓
HideMe() → OnHide()
(反复隐藏/显示)
↓
销毁前 → OnDestroyPanel()
每个阶段均在面板类中以虚方法提供,可重写实现自定义逻辑。
2. Unity 原生生命周期 (Awake)
protected override void Awake()
{
base.Awake(); // 【重要】必须调用基类,否则自动绑定失效!
// 自定义初始化逻辑
}
特别注意:
BasePanel 利用 Awake 进行 Button、Text 等控件的 自动查找与绑定。
如果你需要在子类中使用 Awake,必须调用 base.Awake(),否则 GetControl<T> 将无法获取到控件。
推荐做法:尽量将业务初始化逻辑放在 ShowMe() 中,由框架统一管理,Awake 仅用于处理 Unity 组件层面的依赖。
3. 面板首次显示
面板首次显示(只调用一次)方法 :ShowMe()
public abstract void ShowMe();
调用时机:
- 面板被第一次实例化并显示时
- 异步加载完成后,刚创建 UI GameObject 时
适用场景:
- UI 业务逻辑初始化
- 注册事件(如果不是自动绑定)
- 初始化动画状态(如重置进度条、设置默认文本)
- 第一次读取配置数据
示例:
public override void ShowMe()
{
InitDropdown();
PlayOpenAnim();
}
该方法 只调用一次,不会在面板再次显示时重复执行。
4. 面板每次显示时调用
public virtual void OnShow() { }
调用时机:
- 面板首次显示(紧接着
ShowMe之后执行) - 已隐藏的面板再次显示
- 通过
ShowPanel再次打开该面板
适用场景:
- 刷新 UI 数据(例如更新金币数量、背包列表)
- 重设 UI 交互状态(如焦点、Tab 页归位)
- 播放入场动画(若需要每次显示都播放)
示例:
public override void OnShow()
{
RefreshMoney();
RefreshPlayerInfo();
}
5. 显示转隐藏时的逻辑
public abstract void HideMe();
调用时机:
UIManager.HidePanel<T>()被调用时- 面板被主动关闭时
适用场景:
- 播放关闭动画
- 隐藏子 UI 弹窗
- 清理临时显示状态
一般用来处理“视觉相关”的隐藏逻辑。
示例:
public override void HideMe()
{
CloseAnim.Play();
}
6. 每次隐藏时调用
public virtual void OnHide() { }
调用时机:
HideMe()执行后立即调用- 每次隐藏都会调用
适用场景:
- 暂停动画 / 倒计时
- 保存 UI 临时状态(如 ScrollView 位置)
- 记录用户输入草稿
- 注销 轻量级 事件
示例:
public override void OnHide()
{
SaveScrollPos();
}
区别:HideMe() 负责“关闭视觉效果”,OnHide() 负责“关闭业务逻辑”。
7. 面板被销毁前
public virtual void OnDestroyPanel() { }
调用时机:
- 使用
HidePanel<T>(isDestroy:true)时 - 切场景并执行
UIManager.ClearAllPanels()时 - 手动
DestroyUI 对象前
适用场景:
- 注销全局事件监听(防止内存泄漏的核心!)
- 释放非托管资源
- 清理 GameObject 依赖
- 保存最终数据到本地
示例:
public override void OnDestroyPanel()
{
myButton.onClick.RemoveAllListeners();
GlobalEvent.Remove("UpdateMoney", RefreshUI);
}
OnDestroyPanel 是最重要的资源回收钩子,务必在此处解绑所有委托和事件。
8. 生命周期调用顺序举例
以下是一个面板典型的生命周期:
第一次打开面板
Instantiate(生成物体)Awake(自动绑定控件)ShowMe(业务初始化)OnShow(刷新数据)
再次打开面板
SetActive(true)OnShow(刷新数据)
隐藏面板
HideMe(播放退场动画)OnHide(逻辑暂停)SetActive(false)
销毁面板
HideMeOnHideOnDestroyPanel(解绑事件)Destroy(销毁物体)
UI 系统保证顺序 严格稳定,不会重复触发或遗漏。
9. 异步加载中的生命周期逻辑
UIManager 内部实现了“面板占位机制” (PanelInfo)。
当一个面板正在异步加载时:
ShowPanelAsync只会触发一次加载,不会重复创建。- 同一面板的后续调用会将回调加入队列,等待第一个加载完成。
- 加载中断保护:若在加载过程中调用了
HidePanel,框架会标记该面板为隐藏。- 当资源加载完毕后,框架检测到隐藏标记,会 直接放弃实例化(不会创建 GameObject),并从字典中移除。
- 此时 不会触发
Awake,ShowMe,OnDestroyPanel等任何生命周期,就像从未加载过一样。
10. 最佳实践总结
| 方法 | 调用次数 | 推荐用途 |
|---|---|---|
| Awake | 1次 | 慎用。若使用必须调用 base.Awake()。仅用于组件自身配置。 |
| ShowMe | 1次 | 初始化。创建 Item 模板、查找非 UI 组件、读取静态配置。 |
| OnShow | 多次 | 刷新。更新金币、更新列表、重置动画、注册临时监听。 |
| HideMe | 多次 | 视觉关闭。播放关闭动画。 |
| OnHide | 多次 | 暂停。停止 Update 轮询、停止音频、保存临时输入。 |
| OnDestroyPanel | 1次 | 清理。注销全局事件 (EventManager)、释放 AssetBundle 引用。 |
11. 完整代码示例
public class InventoryPanel : BasePanel
{
// 1. Awake: 尽量不写,如果写一定要调 base
protected override void Awake()
{
base.Awake();
// 这里的代码会在 ShowMe 之前执行
}
// 2. ShowMe: 初始化(仅一次)
public override void ShowMe()
{
InitSlots(); // 创建格子
LoadConfig(); // 读取配置
}
// 3. OnShow: 每次打开刷新数据
public override void OnShow()
{
RefreshItems(); // 重新读取背包数据并显示
PlayEnterAnim(); // 播放弹窗进入动画
// 注册数据监听
GlobalData.OnInventoryChange += RefreshItems;
}
// 4. HideMe: 视觉关闭
public override void HideMe()
{
// 可以在这里播放关闭动画
}
// 5. OnHide: 逻辑暂停
public override void OnHide()
{
// 移除数据监听(防止后台刷新浪费性能)
GlobalData.OnInventoryChange -= RefreshItems;
}
// 6. OnDestroyPanel: 彻底销毁前
public override void OnDestroyPanel()
{
// 兜底解绑,确保没有内存泄漏
GlobalData.OnInventoryChange -= RefreshItems;
Debug.Log("面板已销毁");
}
}