技术博客

Slate 架构:SWidget 的 Invalidate 与 Paint 模型

第 87 / 100 篇 · 2026-03-27 · ue5 · SlateUMGUI架构

Slate 常被一句话概括成"立即模式 UI",这个说法只对了一半。它确实有立即模式的骨架——每帧从根遍历执行布局与绘制——但 widget 树本身是持久保留的:SNew(SVerticalBox) 创建的 SWidget 挂在树上直到显式移除,持有槽位、属性和子节点。真正的机制是保留树 + 每帧遍历 + 失效驱动的增量跳过。Slate 代码在 Engine/Source/Runtime/SlateCore 和 Slate,这篇讲核心三件套。

一次帧内遍历的三个阶段

FSlateApplication::Tick 驱动 UI 帧,内部依次走:

  1. Tick:每个 widget 的 SWidget::Tick 被调(输入处理、动画更新)。大部分 widget 不重写它。
  2. Prepass(布局):自顶向下算期望尺寸(OnComputeDesiredSize 被缓存,只有 layout 失效才重算),然后 OnArrangeChildren 自上而下分配实际几何 FGeometry。这就是 UI 版的 measure/arrange,和 WPF/Android 同款两段式。
  3. Paint(绘制):从根递归 OnPaint,把图元(box/text/shader)追加进 FSlateWindowElementList,之后交给 Slate 渲染器批处理成 draw call。

三个阶段各有独立的失效域,这是 Slate 最重要的设计决定。

Invalidate:失效标志位

SWidget::Invalidate(EInvalidateWidget::Type) 接收一组位标志(EInvalidateWidgetReason),每个标志对应一个可跳过的阶段:

  • Layout:几何依赖变了(文字内容、子节点数量),Prepass 需要重算这条链。
  • Paint:外观变了(颜色、笔刷),布局没变,只重绘。
  • ChildOrder:子节点增删。
  • Visibility:可见性切换(影响布局跳过)。
  • AttributeRegistration:绑定的属性(TAttribute)需要重新求值。

标志沿 widget 的无效化路径向上冒泡:子节点 Paint 失效会让父链上所有 paint 缓存失效,但不会触碰 layout 链。这样一次失效只触发必要阶段的局部重算。UE5 的 FSlateInvalidationWidgetList 把树上每个 widget 登记成带序号的槽位,失效记录到 FWidgetProxy,遍历时用 proxy 的 FastPath 判断"这一段缓存能不能直接用"——这就是所谓 Fast Path:缓存有效的子树整棵跳过 OnPaint。

Volatility:缓存的例外条款

纯缓存模型有个致命问题:有些 widget 的绘制结果不可预测地变——绑定属性的外部数据每帧都可能不同。这类 widget 必须声明 SetVolatile(true)(或 SetVolatileIndividually), volatile 节点及其子树永远绕过缓存,每次 Paint 都真算。

这是性能优化里最常见的坑:SInvalidationPanel 把子树画进缓存,只要里面任何一个 widget 是 volatile,整棵子树的缓存就形同虚设。排查套路是用 Slate 的 invalidation 调试 HUD(Slate.EnableInvalidationPanelDebugging 控制台变量)看哪些节点走了慢路径——肉眼可见一片红色 volatile 时,说明绑定写法出了问题(见 UMG 属性绑定那篇)。

和 UMG 的衔接

UMG 的 UUserWidget 并不是 Slate 的替代品,而是包了一层:UMG 对象树(UWidgetTree)在 RebuildWidget 时生成对应的 Slate widget 树,UWidget 负责状态和序列化,Slate 负责帧内布局与绘制。所以 UMG 的性能问题最终几乎都能翻译成 Slate 的问题:绑定太频繁 → volatile 太多;层级太深 → Paint 遍历基数大;Retainer Box → 强制离屏渲染整块子树。

Slate 一帧:三个阶段 + 失效标志决定走哪条路 1. Tick 输入/动画 2. Prepass 布局 measure + arrange 3. Paint 绘制 产出 DrawElement 渲染器 批处理 失效标志(沿父链冒泡,各管一段): Layout → 只重算 Prepass Paint → 只重算 Paint ChildOrder / Visibility / AttributeRegistration → 各归其位 Fast Path:缓存有效的子树整棵跳过 Volatile:声明不可预测,永远绕过缓存 调试:Slate.EnableInvalidationPanelDebugging SInvalidationPanel 缓存整棵子树;内部含 volatile 节点时缓存失效

给使用者的三条推论

第一,UI 卡顿先怀疑 Paint 遍历基数:widget 数量 × 每帧真实执行 OnPaint 的比例。HUD 上几百个血条不重缓存,任何旗舰机都顶不住。

第二,失效标志比手动刷新细。改颜色就让它只失效 Paint;想清楚你改的东西影响哪一阶段,比无脑 Invalidate 全部省的是 Prepass 整棵链。

第三,层级深本身不贵,贵的是每层都失效。把高频变化的东西(伤害数字、进度条)隔离在浅层小缓存里,让低频的框架部分永远走 Fast Path,比到处贴 Retainer Box 干净得多。