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()
  • 手动 Destroy UI 对象前

适用场景:

  • 注销全局事件监听(防止内存泄漏的核心!)
  • 释放非托管资源
  • 清理 GameObject 依赖
  • 保存最终数据到本地

示例:

public override void OnDestroyPanel()
{
    myButton.onClick.RemoveAllListeners();
    GlobalEvent.Remove("UpdateMoney", RefreshUI);
}

OnDestroyPanel 是最重要的资源回收钩子,务必在此处解绑所有委托和事件


8. 生命周期调用顺序举例

以下是一个面板典型的生命周期:

第一次打开面板

  1. Instantiate (生成物体)
  2. Awake (自动绑定控件)
  3. ShowMe (业务初始化)
  4. OnShow (刷新数据)

再次打开面板

  1. SetActive(true)
  2. OnShow (刷新数据)

隐藏面板

  1. HideMe (播放退场动画)
  2. OnHide (逻辑暂停)
  3. SetActive(false)

销毁面板

  1. HideMe
  2. OnHide
  3. OnDestroyPanel (解绑事件)
  4. Destroy (销毁物体)

UI 系统保证顺序 严格稳定,不会重复触发或遗漏。


9. 异步加载中的生命周期逻辑

UIManager 内部实现了“面板占位机制” (PanelInfo)。

当一个面板正在异步加载时:

  1. ShowPanelAsync 只会触发一次加载,不会重复创建。
  2. 同一面板的后续调用会将回调加入队列,等待第一个加载完成。
  3. 加载中断保护:若在加载过程中调用了 HidePanel,框架会标记该面板为隐藏。
    • 当资源加载完毕后,框架检测到隐藏标记,会 直接放弃实例化(不会创建 GameObject),并从字典中移除。
    • 此时 不会触发 Awake, ShowMe, OnDestroyPanel 等任何生命周期,就像从未加载过一样。

10. 最佳实践总结

方法调用次数推荐用途
Awake1次慎用。若使用必须调用 base.Awake()。仅用于组件自身配置。
ShowMe1次初始化。创建 Item 模板、查找非 UI 组件、读取静态配置。
OnShow多次刷新。更新金币、更新列表、重置动画、注册临时监听。
HideMe多次视觉关闭。播放关闭动画。
OnHide多次暂停。停止 Update 轮询、停止音频、保存临时输入。
OnDestroyPanel1次清理。注销全局事件 (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("面板已销毁");
    }
}