• UGUI性能优化学习笔记(一)网格重建


    一、基本概念

    在正式学习UGUI性能优化之前,需要先了解一些基本的概念

    • 网格

    无论是3D物体还是2D物体,都是由网格绘制而成。需要绘制的网格越多,性能消耗越大。
    Unity编译器调整到Wireframe模式,可以查看当前场景元素的网格组成

    下面是一个默认的Image和一个默认的Text网格数量的对比

    • Draw Call

    Draw Call指在渲染流水线中,CPU向GPU发送的一条指令。通过这条指令,CPU可以通知GPU渲染指定的图元列表

    • 填充率

    填充率是指显卡每帧或每秒能够渲染的像素数量。如果一个像素被重复渲染了多次,那么它必然会占用更多的资源。
    在Unity编译器中开启Overdraw模式,可以查看有哪些像素存在重复渲染

    我们将两个Image的一部分重叠放置,就可以观察到重叠部分的颜色会更深一些

    • 批处理

    批处理就是我们常听的Batch,或者合批。批处理就是把渲染时使用相同材质(Shader)、相同贴图的3D模型的网格合并在一起,成为一个大网格,然后再调用一次Draw Call,直接渲染这一个大网格。这样做可以降低Draw Call的数量,以优化性能。

    二、网格重建

    在UGUI中,Canvas负责将其下的子UI元素进行合批操作,也就是Batch。当子UI元素发生了变化时,Canvas就需要重新进行Batch操作。Batch操作具体到各个子元素上,就是执行它们各自的Rebuild操作,重新计算元素的布局和网格。Batch和Rebuild加起来构成了所谓的网格重建。

    下面我们通过代码跟踪一下整个过程

    2.1 Batch

    首先在Canvas类中,当Canvas需要进行网格重建时,会调用SendWillRenderCanvases()方法

    [RequiredByNativeCode]
    private static void SendWillRenderCanvases()
    {
      Canvas.WillRenderCanvases willRenderCanvases = Canvas.willRenderCanvases;
      if (willRenderCanvases == null)
    	return;
      willRenderCanvases();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Canvas.willRenderCanvases这个事件是在CanvasUpdateRegistry这个类中注册的。CanvasUpdateRegistry采用了单例模式。它相当于UI元素与Canvas之间的中介,UI元素可以通过它来注册自己的Rebuild方法。

    public class CanvasUpdateRegistry  
    {  
        private static CanvasUpdateRegistry s_Instance;
        // ...
    	protected CanvasUpdateRegistry()
    	{
    		Canvas.willRenderCanvases += PerformUpdate;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    CanvasUpdateRegistry内部提供了两个队列用来保存需要重建的布局元素(通过LayoutGroup布局改变的UI)和Graphics元素(Image、Text等)。UI元素通过CanvasUpdateRegistry暴露的注册API,来将自己添加到这两个队列中。

    private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();  
    private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();
    
    • 1
    • 2

    接下来是重头戏PerformUpdate(),也就是被注册到Canvas.willRenderCanvases事件的方法。它主要分为三部分,我通过注释的方式予以体现

    private void PerformUpdate()
    {
    	UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
    	// 清除两个队列中无用的数据,比如已置空或已销毁
    	CleanInvalidItems();
    
    	m_PerformingLayoutUpdate = true;
    	// 将layout队列按照层级进行排序(越是父级越靠前)
    	m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
    
    	// 第一部分:依次调用layout队列中元素的Rebuild()方法
    	for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
    	{
    		UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);
    
    		for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
    		{
    			var rebuild = m_LayoutRebuildQueue[j];
    			try
    			{
    				if (ObjectValidForUpdate(rebuild))
    					rebuild.Rebuild((CanvasUpdate)i);
    			}
    			catch (Exception e)
    			{
    				Debug.LogException(e, rebuild.transform);
    			}
    		}
    		UnityEngine.Profiling.Profiler.EndSample();
    	}
    
    	for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
    		m_LayoutRebuildQueue[i].LayoutComplete();
    	// 清空layout队列
    	m_LayoutRebuildQueue.Clear();
    	m_PerformingLayoutUpdate = false;
    	UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
    	UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Render);
    
    	// 第二部分:剔除可剪切元素
    	// now layout is complete do culling...
    	UnityEngine.Profiling.Profiler.BeginSample(m_CullingUpdateProfilerString);
    	ClipperRegistry.instance.Cull();
    	UnityEngine.Profiling.Profiler.EndSample();
    
    	m_PerformingGraphicUpdate = true;
    
    	// 第三部分:依次调用Graphics队列中元素的Rebuild()方法
    	for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
    	{
    		UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);
    		for (var k = 0; k < m_GraphicRebuildQueue.Count; k++)
    		{
    			try
    			{
    				var element = m_GraphicRebuildQueue[k];
    				if (ObjectValidForUpdate(element))
    					element.Rebuild((CanvasUpdate)i);
    			}
    			catch (Exception e)
    			{
    				Debug.LogException(e, m_GraphicRebuildQueue[k].transform);
    			}
    		}
    		UnityEngine.Profiling.Profiler.EndSample();
    	}
    
    	for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
    		m_GraphicRebuildQueue[i].GraphicUpdateComplete();
    	// 清空Graphics队列
    	m_GraphicRebuildQueue.Clear();
    	m_PerformingGraphicUpdate = false;
    	UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Render);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    2.2 Rebuild

    我们先来看Layout的Rebuild过程。该方法位于LayoutRebuilder类中

    public void Rebuild(CanvasUpdate executing)
    {
    	switch (executing)
    	{
    		case CanvasUpdate.Layout:
    		
    			PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement)
    			.CalculateLayoutInputHorizontal());
    			
    			PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController)
    			.SetLayoutHorizontal());
    			
    			PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement)
    			.CalculateLayoutInputVertical());
    			
    			PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController)
    			.SetLayoutVertical());
    			break;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这个方法主要执行的逻辑是一系列计算,包括自下而上计算布局大小、行列数(CalculateLayoutInputHorizontalCalculateLayoutInputVertical)和自下而上调整子物体位置或调整自身大小(SetLayoutHorizontalSetLayoutVertical)等。

    各Layout元素在设置为脏数据时,通过LayoutRebuilder类中的静态方法MarkLayoutForRebuild()将自己标记为需要重新计算布局的元素。比如LayoutGroup类的SetDirty()方法

    protected void SetDirty()
    {
    	if (!IsActive())
    		return;
    
    	if (!CanvasUpdateRegistry.IsRebuildingLayout())
    		LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
    	else
    		StartCoroutine(DelayedSetDirty(rectTransform));
    }
    
    IEnumerator DelayedSetDirty(RectTransform rectTransform)
    {
    	yield return null;
    	LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    由此可见,对于Layout元素,每一次重建都需要进行大量的计算以确定新的布局。因此在项目中应该尽量减少使用这类布局组件。

    接下来看Graphics元素。可以看到,这类元素在重建时主要涉及到更新顶点和材质的脏数据。

    public virtual void Rebuild(CanvasUpdate update)
    {
    	if (canvasRenderer == null || canvasRenderer.cull)
    		return;
    
    	switch (update)
    	{
    		case CanvasUpdate.PreRender:
    			if (m_VertsDirty)
    			{
    				// 更新顶点
    				UpdateGeometry();
    				m_VertsDirty = false;
    			}
    			if (m_MaterialDirty)
    			{
    				// 更新材质
    				UpdateMaterial();
    				m_MaterialDirty = false;
    			}
    			break;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    当Graphics元素发生颜色变换或大小改变时,会将顶点标记为脏数据。当材质发生改变时,会将材质标记为脏数据。

    值得注意的是,当元素触发OnEnable()(除此之外,还包括OnTransformParentChanged()OnDidApplyAnimationProperties()等)时,会触发SetAllDirty()方法。该方法会将所有数据全部标记为脏数据

    public virtual void SetAllDirty()
    {
    	if (m_SkipLayoutUpdate)
    	{
    		m_SkipLayoutUpdate = false;
    	}
    	else
    	{
    		SetLayoutDirty();
    	}
    
    	if (m_SkipMaterialUpdate)
    	{
    		m_SkipMaterialUpdate = false;
    	}
    	else
    	{
    		SetMaterialDirty();
    	}
    
    	SetVerticesDirty();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    因此通过SetActive()方式控制UI元素的显隐也可能会造成性能问题。

    三、总结

    最后来总结一下。

    首先我们知道了Canvas下的子元素发生改变时,会触发整个Canvas的重建操作。因此将所有的UI元素全部堆砌在一个Canvas下显然会造成性能问题。合理的做法应该是将静态的UI元素与动态的UI元素分离到不同的Canvas下,也就是我们常说的动静分离,从而避免大量无意义的重建。

    其次,对于Layout元素在重建过程中需要进行大量的计算工作,所以应该减少Layout组件的使用。

    最后,Graphics元素在OnEnable()时也会进行重建,因此通过SetActive()方式控制复杂UI的显隐也可能会造成性能问题。

    四、参考资料

    [1]. https://blog.csdn.net/aaakkk_1996/article/details/123068009
    [2]. https://www.sikiedu.com/course/538
    [3]. https://blog.csdn.net/sinat_25415095/article/details/112388638

  • 相关阅读:
    【JAVAWEB开发】基于Java+Servlet+Ajax+jsp网上购物系统设计实现
    JuiceFS 在多云存储架构中的应用 | 深势科技分享
    OpenJudge NOI题库 1.7 编程基础之字符串
    推出一系列GaN功率放大器: QPA2211、QPA2211D、QPA2212、QPA2212D、QPA2212T,支持卫星通信和5G基础设施。
    Android之使用GirdLayoutManager时候给Item设置边距
    【springboot】4、容器功能
    软件安全测试为什么重要?安全测试应该怎么进行?
    指针进阶2
    如何优雅的使用MyBatis?
    java基于springboot家庭水电燃气网上交费系统
  • 原文地址:https://blog.csdn.net/LWR_Shadow/article/details/128101839