Canvas
Canvas是用来放置所有UI元素的地方,所有的UI元素必须是这样一个Canvas的子对象。它给Unity的渲染系统提供按层划分的几何系统,负责将其内部的几何形状合并到批处理、生成合适的渲染指令并发送到Unity图形系统。
问题
当UI元素变化时,它会重新生成网格并向GPU发起绘图调用,从而显示出UI。
生成这些网格会消耗大量性能,需要将UI元素收集到批处理中,从而尽可能减少绘图调用。因为批处理的生成过程性能消耗较大,通常只在必要时候才重新生成。问题在于,当画布上有一个或多个元素变化时,必须重新分析整块画布,才能得到绘制元素的最优方法。
许多用户将整个游戏的UI都放到一块画布,上面摆放了成百上千个元素。因此当修改其中一个元素时,会产生持续数毫秒的CPU使用量飙升情况。
解决方案:划分画布
每块画布上的元素都与其它画布的元素相隔离,所以我们可以使用工具来切分画布,从而解决Unity UI的批处理问题。
我们也可以通过嵌套画布来解决,这样允许设计师创建大型分层UI,而且不必担心不同内容出现在多个画布上。子画布的内容与父画布和同级画布相互隔离,它们会保持自带几何体,执行自己的批处理。
当使用子画布分离画布时,尝试根据画布更新时间来分组。例如:分离动态元素和静态元素。
子画布(Sub-canvas) 是嵌套在其他画布组件内部的画布组件。子画布能够将其孩子节点与其父画布隔离开,一个被标记为脏的子节点不会迫使其父画布重新构建几何内容,反之亦然。有几种特殊情况会使上述情形失效,比如,改变父画布导致子画布改变尺寸。
从源码的角度理解
何时会重新批处理(rebatch)
当一个Canvas被标记为脏的,一般分两种情况:
1.修改了宽高这样会影响顶点位置需要重建Mesh.
2.仅仅只修改了显示元素。
此时unity会在代码中区分对待。
Canvas源码
Canvas功能都由C++代码完成,没有公开源码。但通过反编译我们可以查看Canvas的部分c#代码。
public sealed class Canvas : Behaviour
{
public delegate void WillRenderCanvases();
//...略
[RequiredByNativeCode]
private static void SendWillRenderCanvases()
{
if (Canvas.willRenderCanvases == null)
return;
Canvas.willRenderCanvases();
}
//强制刷新所有Canvas,可以在外部调用
public static void ForceUpdateCanvases()
{
Canvas.SendWillRenderCanvases();
}
}
当Canvas需要重绘时候会调用SendWillRenderCanases()方法,
ugui内部在CanvasUpdateRegistery的构建函数中将Canvas.willRenderCanvas事件添加到了this.PerformUpdate方法。
CanvasUpdateRegistery源码
public class CanvasUpdateRegistry
{
protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}
private void PerformUpdate()
{
//开始BeginSample
//在Profiler中看到的标志性函数Canvas.willRenderCanvases耗时就在这里
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
CleanInvalidItems();
m_PerformingLayoutUpdate = true;
//需要重建的布局元素(RectTransform发生变化),首先需要根据子对象的数量对它排序
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
//遍历脏Layout 重建他们的布局
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;
//所有已注册的裁剪组件(例如Mask)都需要剔除全部被裁减的组件
// now layout is complete do culling...
ClipperRegistry.instance.Cull();
m_PerformingGraphicUpdate = true;
//脏的Graphic组件需要重建它们的图形元素
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))
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);
}
}
重建过程中进行了Graphic组件中的Layout和网格的重新计算,这一过程在 CanvasUpdateRegistry 类中执行。CanvasUpdateRegistry是一个C#类,它的源代码可以在Unity’s Bitbucket上查看。
在 CanvasUpdateRegistry 值得注意的方法是 PerformUpdate 。这个方法会在画布组件调用 WillRenderCanvases 事件时被调用。这个事件每帧调用一次。
PerformUpdate 会进行3步处理:
- 脏Layout组件需要通过 ICanvasElement.Rebuild 方法重建它们的布局(layout)。
- 所有已注册的裁剪组件(例如Mask)都需要剔除全部被裁减的组件,由ClippingRegistry.Cull方法完成。
- 脏的Graphic组件需要重建它们的图形元素。
Layout和Graphic的重建过程会被拆分成多个部分。Layout重建分3步完成(PreLayout,Layout和PostLayout),Graphic重建分2步完成(PreRender和LatePreRender)。
Layout重建
必须根据Layout层级顺序计算那些包含在Layout中的组件的位置和尺寸。在Game Object层级中,离根节点近的Layout有可能会改变嵌套在在它里面的Layout的位置和尺寸,所以它需要被先计算。
为此,UI系统依据Layout在层级中的深度对脏Layout列表中的Layout进行排序,高层的(例如,父Transform更少)的项会被移动到列表的前面。
排序后的Layout组件列表接下来要重建布局。这时被Layout组件控制的UI元素的位置和尺寸会发生改变。有关Layout如何影响每个元素的位置的详细叙述,请查看Unity手册中的UI Auto Layout。
Graphic重建
当Graphic组件重建时,UI系统将控制传递给ICanvasElement接口的Rebuild方法。Graphic类实现了这一方法并且在Rebuild过程的PreRender阶段执行两个不同的重建步骤。
- 如果顶点数据被标记为脏数据(例如,组件的RectTransform改变尺寸),网格会重建。
- 如果材质数据被标记为脏数据(例如,组件的材质或纹理改变),所附加的CanvasRenderer的材质会被更新。
Graphic重建不通过任何特定顺序的图形组件列表进行,也不需要进行任何排序操作。