制作UI过程,添加Canvas组件将会打断和之前元素DrawCall的合并,每个Canvas都会开始一个全新的DrawCall,当Canvas需要重绘的时候会调用SendWillRenderCanvases()方法。
Canvas.cs(部分源码)
//部分源码
public sealed class Canvas : Behaviour
{
public delegate void WillRenderCanvases();
//公有事件,在CanvasUpdateRegistry.cs的构造函数里,为willRenderCanvases事件添加了一个监听PerformUpdate。
//在渲染(所有)Canvas之前会抛出willRenderCanvases事件,继而调用到CanvasUpdateRegistry.cs的PerformUpdate方法
public static event Canvas.WillRenderCanvases willRenderCanvases;
//强制更新所有Canvas, 当Canvas需要重新绘制时调用,可在外部主动调用
public static void ForceUpdateCanvases()
{
Canvas.SendWillRenderCanvases();
}
private static void SendWillRenderCanvases()
{
if (Canvas.willRenderCanvases != null)
{
Canvas.willRenderCanvases();
}
}
}
CanvasUpdateRegistry.cs
//部分源码
//这个枚举在后面遍历元素Rebuild的时候会用到
public enum CanvasUpdate
{
//布局的三个阶段
Prelayout = 0,
Layout = 1,
PostLayout = 2,
//渲染的两个阶段
PreRender = 3,
LatePreRender = 4,
//
MaxUpdateValue = 5
}
public class CanvasUpdateRegistry
{
//布局重建队列
private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();
//图形重建队列
//简单说明下IndexedSet,是一个容器,内部使用List和Dictionary实现了快速增删改查
private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();
//为什么UI发生变化(布局变化、顶点变化、材质变化)一定要加入到这两个等待重建队列?
//一个UI界面同一帧内可能有很多元素发生变化,同步随时重建肯定会卡死
//所以需要加入到待重建队列等待一个时机(下一帧开始渲染之前)统一来处理
protected CanvasUpdateRegistry()
{
//在构造里监听了Canvas.willRenderCanvases,调用this.PerformUpdate()
Canvas.willRenderCanvases += PerformUpdate;
}
private void PerformUpdate()
{
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
CleanInvalidItems();
//一个UI对象(如Image),RectTransform和Graphics同时发生修改时,更新的含义不同,需要区分对待
//所以会维护两个队列m_LayoutRebuildQueue和m_GraphicRebuildQueue
//1、修改了Image的宽高,这样Mesh网格的顶点会发生变化,此时需要加入m_LayoutRebuildQueue队列处理
//2、修改了Image的Sprite,并不会影响顶点位置信息,此时需要加入m_GraphicRebuildQueue队列处理
//所以在遍历队列更新的时候会分层处理
//1、重建布局:分别以Prelayout、Layout、PostLayout为参数重建
//2、重建图形:分别以PreRender和LatePreRender为参数重建
//保证Image知道现在是要更新布局,还是更新渲染,并且明确处于什么阶段
m_PerformingLayoutUpdate = true;
//需要重建布局的元素(RectTransform变化),根据父物体的数量来排序
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
//遍历待重建布局元的素队列
for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
{
for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
{
var rebuild = instance.m_LayoutRebuildQueue[j];
try
{
if (ObjectValidForUpdate(rebuild))
//重新布局
rebuild.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
}
//重新布局完成函数
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue[i].LayoutComplete();
//重新布局完成后清空布局元素队列
instance.m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
// 布局构建结束后,开始进行Mask2D裁剪
ClipperRegistry.instance.Cull();
m_PerformingGraphicUpdate = true;
//需要重新布局的Graphices元素(Image Text RawImage发生变化)
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
{
try
{
var element = instance.m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
//重建UI元素
element.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
}
}
}
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue[i].GraphicUpdateComplete();
instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}
}
CanvasUpdateSystem 画布刷新流程(类图)
ICanvasElement
- 重建方法:void Rebuild(CanvasUpdate executing);
- 布局重建完成:void LayoutComplete();
- 图像重建完成:void GraphicUpdateComplete();
- 检查Element是否无效:bool IsDestroyed();
CanvasUpdateRegistry 被初始化时(构造函数)向Canvas中注册了更新函数(PerformUpdate),以用来响应重建。
Canvas在渲染前会调用willRenderCanvases,即执行PerformUpdate ,流程如下:
脏标记
标记延迟执行,优化重新渲染的手段。
详情请见:游戏设计模式:脏标识模式
例如在Graphic 中存在三种脏标分别代表三种等待重建
- 尺寸改变时(RectTransformDimensions):LayoutRebuild 布局重建
- 尺寸、颜色改变时:Vertices to GraphicRebuild 图像重建
- 材质改变时:Material to GraphicRebuild 图像重建
- 层级改变、应用动画属性(DidApplyAnimationProperties) :All to Rebuild 重建所有