Unity UI优化--Canvas合批--从源码角度解读

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重建不通过任何特定顺序的图形组件列表进行,也不需要进行任何排序操作。

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KindSuper_liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值