ARTICLE · 1079626
【Unity UGUI源码深度解析】04|CanvasUpdateRegistry源码解析:一帧中的布局、裁剪与图形重建如何调度
《UGUI源码深度解析》第 4 篇 · 界面小组工作日志基准:Unity 2022.3.62f2c1 / 本地 UGUI 1.0.0。人物与项目情节为虚构;源码机制以本地实现为准。

一、三个人同时改背包,谁先干活?
装备名变长,详情面板需要长高;面板长高,遮罩范围又跟着变化;最后,文字和背景才知道该准备什么网格。
阿澈问:“如果先画了,再排版,岂不是白忙一遍?”
这正是 CanvasUpdateRegistry 值得单独读一篇的原因。它不负责计算每个字的位置,却决定什么工作先发生。上一回的 Dirty,只是把需求交给了这个调度者。
构造函数把 PerformUpdate 挂到 Canvas.willRenderCanvases。这里应理解为渲染前更新入口,而不是死记“每帧严格执行一次”。主动刷新 Canvas 等操作会影响调用时机,查问题时必须结合栈和帧号。
二、不是一个大队列轮流抽签
注册表持有布局队列和图形队列,元素通过 ICanvasElement 暴露 Rebuild、完成通知与销毁检查。布局队列还会先排序:比较 Transform 的父节点数量,让浅层根节点排在深层之前。
“这就等于布局从父到子了?”
还不能这样概括。这里只是不同队列元素之间的排序。单个 LayoutRebuilder 内部如何遍历子树,是另一层算法;它的测量恰恰需要先了解孩子。队列排序与子树遍历不能混成一句话。
PerformUpdate 的布局阶段循环摘录如下:
for(int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)这覆盖 Prelayout、Layout、PostLayout。随后发出 LayoutComplete,清空布局队列,再执行 ClipperRegistry.instance.Cull。最后处理 PreRender、LatePreRender,发出 GraphicUpdateComplete 并清空图形队列。

五个阶段不意味着每个组件必须执行五份逻辑。Graphic 主要在 PreRender 更新网格与材质,LayoutRebuilder 主要响应 Layout,ScrollRect 又有自己的阶段处理。阶段是协议,不是平均分工。
三、为什么裁剪夹在中间?
我把装备详情的矩形画在纸上。文字换行后,面板高度才能确定;高度确定,才知道哪些内容在视口里;可见性确定,再准备要提交的图形数据。
布局之后裁剪,避免拿旧矩形决定可见区域。裁剪之后更新图形,也让 Graphic 能根据 cull 状态跳过部分工作。这里存在依赖关系,不是源码作者为了整齐把函数排成三段。
但 Cull 不会删掉列表项,也不是完整 GPU 遮挡剔除系统。它调度已注册的裁剪组件;具体矩形求交、目标通知等工作仍由 RectMask2D 等类型完成。
四、重复登记能合并,迟到登记有风险
队列使用 IndexedSet 维护唯一性。图形注册最终调用 AddUnique,同一元素重复标脏通常不会在同一队列里排成一长串。布局根相同的请求也可以汇合。
阿澈想到了一个主意:“那我在重建时发现还有事,再登记一次?”
先看这个完整源码分支:
if(m_PerformingGraphicUpdate){ Debug.LogError(string.Format("Trying to add {0} for graphic rebuild while we are already inside a graphic rebuild loop. This is not supported.", element)); return false;}图形重建期间不支持重新注册。写 OnPopulateMesh 时顺手修改另一个 Graphic 的尺寸或颜色,可能就撞到这里。标记还在不在、队列有没有接受,必须分开检查。
更容易读错的是布局注册:本版本对应的 m_PerformingLayoutUpdate 检查被包在注释中,不能写成“两个队列都严格禁止重建期间注册”。不过这也不是邀请我们依赖重入。新元素可能错过排序或部分阶段,末尾还会清队列,延迟到下一次稳定更新更容易推理。
五、待办里的对象已经销毁怎么办?
CleanInvalidItems 在更新前清理空条目与 IsDestroyed 返回 true 的元素。通过接口持有 Unity 对象时,普通判空不足以代替原生对象存活检查,这与第 2 篇的 IsDestroyed 接上了。
调用单个元素 Rebuild 时还有异常捕获,错误会记录对应对象,避免一个异常直接打断整个循环。然而“调度循环继续”不等于“坏掉的组件自动恢复”。异常前写入了一半状态,依然需要我们修复。
这也解释了排查布局错误时为何应先清 Console:一个尺寸没有更新,可能是前面的计算抛了异常,而不是排序不够智能。
六、按阶段看一次长名字
准备 VerticalLayoutGroup、自动换行 Text 和受控宽度,设置断点在 PerformUpdate、LayoutRebuilder.Rebuild、RectMask2D.PerformClipping、Graphic.Rebuild。改变文本,记录调用顺序与阶段值,观察高度是在图形提交之前确定的。
再对同一 Image 连续标脏,检查图形队列中的实例是否重复。最后在独立测试组件中尝试重建时注册,观察错误。
以上是可复现方案,Profiler 能辅助观察成本,但不能把一次断点暂停后的耗时当成真实性能。
阿澈把今天的结论写成一行:“先量房,再划范围,最后备料。”
顺序清楚了,下一篇换个问题:换了一块屏幕,整个背包为什么忽大忽小?我们去看 CanvasScaler。
本篇源码索引
CanvasUpdateRegistry: ./com.unity.ugui@1.0.0/Runtime/UI/Core/CanvasUpdateRegistry.csLayoutRebuilder: ./com.unity.ugui@1.0.0/Runtime/UI/Core/Layout/LayoutRebuilder.cs