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

news2024/11/25 2:52:15

一、基本概念

在正式学习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();
}

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

public class CanvasUpdateRegistry  
{  
    private static CanvasUpdateRegistry s_Instance;
    // ...
	protected CanvasUpdateRegistry()
	{
		Canvas.willRenderCanvases += PerformUpdate;
	}
}

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>();

接下来是重头戏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);
}

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;
	}
}

这个方法主要执行的逻辑是一系列计算,包括自下而上计算布局大小、行列数(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);
}

由此可见,对于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;
	}
}

当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();
}

因此通过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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/45716.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

冯诺依曼体系结构及操作系统的认识

目录1.前言2.冯诺依曼体系结构2.1.结构构成2.2.硬件分析2.2.1存储器的作用2.2.2CPU2.3.实际分析3.操作系统3.1.是什么3.2.为什么3.3.操作系统怎么进行管理3.3.1管理的本质3.3.2管理的方法3.4.系统调用3.5.最终体系1.前言 为什么现代计算机都被称为冯诺依曼结构计算机&#xff1…

C#运算符执行顺序对照表

C#运算符执行顺序对照表&#xff1a;在线查看C#运算符执行优先级别 窍门&#xff1a; CtrlF 快速查找 C#运算符优先级&#xff0c;是描述在计算机计算表达式时执行运算的先后顺序。 先执行具有较高优先级的运算&#xff0c;然后执行较低优先级的运算。 例如&#xff0c;我们常说…

k8s整合kong

k8s整合kong Kong网关的发展历程 ​ Kong网关起源于2007年&#xff0c;由Augusto、Marco、Michele三人在意大利的一个小车库中开发&#xff0c;当时命名为Mashup平台。在随后7年的时间里&#xff0c;Mashup平台逐渐占据API网关市场的主导地位。2017年10月&#xff0c;Mashup平台…

引擎入门 | Unity UI简介–第2部分(2)

本期我们继续为大家进行Unity UI简介&#xff08;第二部分&#xff09;的后续教程 本篇内容 3.动画按钮滑入 文章末尾可免费获取教程源代码 本篇本篇Unity UI简介&#xff08;第二部分&#xff09;篇幅较长&#xff0c;分为八篇&#xff0c;本篇为第二篇。 3.动画按钮滑入…

如何安装Torch7在Ubuntu20.04 ( CUDA10.1 和 CUDNN7.6.5)

先展示安装成果&#xff0c;东西没啥&#xff0c;就是很麻烦&#xff0c;特别是安装torch7库&#xff0c;下载不下来&#xff0c;断断续续的。 1. 首先&#xff0c;安装CUDA 10.1CUDNN7.6.5。切记&#xff1a;cudnn不要装cudnn8.X&#xff0c;好像跟torch不是很匹配。另外就是g…

省市县:数十万数据集PM2.5面板数据柵格数据(1998-2019)

1、数据来源&#xff1a; https://sites.wustl.edu/acag/datasets/surface-pm2-5/ 2、时间跨度&#xff1a;1998-2019 3、区域范围&#xff1a;中国各省、各城市、各区县 4、指标说明&#xff1a; 根据Global/Regional Estimates (V5.GL.02)&#xff0c;计算出国内PM2.5数…

车载电子专用DC-DC方案PL5501

PL5501是一个同步4开关Buck-Boost能够调节输出电压的控制器高于或低于输入电压。PL5501运作输入电压范围从3.6 V到32 V (36 V Maximum)以支持各种应用程序。PL5501 buck采用恒ON时间控制&#xff0c;上位机采用升压和升压两种操作方式负荷和线路调节。开关频率可以设置为150kHz…

在字符串两侧填充指定字符ljust()与rjust()方法

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 在字符串两侧填充指定字符 ljust()与rjust()方法 选择题 以下python代码输出正确的一项是? str"abc" print("【显示】str原始数据&#xff1a;") print("【执行】…

Spark框架

Spark计算速度 Hadoop的多个job之间的数据通信是基于磁盘的 Hadoop偏存储&#xff0c;其MR框架&#xff0c;是基于磁盘的计算&#xff0c;多个MR作业之间的数据交互&#xff0c;依赖于磁盘的IO&#xff0c;这会影响计算性能。 job1&#xff1a;读取磁盘文件&#xff0c;MR计算…

网页JS自动化脚本(三)查找定位页面元素的多种方法

当然定位元素不止一个方法,下面总结一些常用的方法 父元素定位 a.undertips-link>span我们看到父元素是第8代的a,那么先定位到a,然后再通过a定位到子元素span,可以看到1 of 1 ,匹配上了唯一的元素 祖父元素定位 div#lm-new>a>span可以看到进对第7代的div元素进行定位…

承上启下:基于全域漏斗分析的主搜深度统一粗排

1. 背景 1.1 概述 淘宝主搜索是一个典型的多阶段检索系统&#xff0c;主要分为召回、粗排、精排等阶段。召回阶段&#xff0c;由文本召回、个性化等多路召回构成&#xff0c;输出商品量级约10^5&#xff1b;粗排阶段&#xff0c;需要从三路召回集合中分别进行筛选&#xff0c…

[附源码]SSM计算机毕业设计校园自行车租售管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Vue 打包优化之 externals 抽离公共的第三方库

使用 vue/cli 脚手架构建的 Vue 全家桶项目&#xff0c;默认配置下&#xff0c;打包后会把 vue、vue-router、axios、vuex、element-ui、echarts 等公共库打包在一起&#xff0c;导致基础 chunk、vendor 包体积特别大&#xff0c;有时一个文件能达到 3-5MB&#xff0c;这会大大…

ThinkPHP和uniapp开发的CRM售后管理系统(客户、合同、工单、任务、报价、产品、库存、出纳、收费)

ThinkPHP和uniapp开发的CRM售后管理系统无加密的开源源码(可用于自营外包项目(多主体)、可用于外包定制开发项目) 主要功能&#xff1a;客户、合同、工单、任务、报价、产品、库存、出纳、收费&#xff0c; 适用于&#xff1a;服装鞋帽、化妆品、机械机电、家具装潢、建材行业…

NR CSI(三) CQI

微信同步更新&#xff0c;欢迎关注同名modem协议笔记 这篇主要看下CQI的相关内容&#xff0c;CQI在spec上描述的内容比较少&#xff0c;主要是和调制方式和码率相关&#xff0c;所以这篇的内容也比较简短。先看下CSI Report Quantity 上报测量量。 很早之前有人问我你知道各个…

【面试题】DOM

1. DOM的本质 DOM(Document Object Model)&#xff0c;文档对象模型。DOM的本质是从HTML文件中解析出来的一棵树。DOM的数据结构是树形结构&#xff08;DOM树&#xff09; 2. DOM节点操作 2.1 获取DOM节点 <!DOCTYPE html> <html lang"en"> <head…

【毕业设计】30-基于单片机矿井瓦斯_气体浓度_烟雾浓度报警设计(原理图+源代码+仿真+答辩论文+答辩PPT)

【毕业设计】30-基于单片机矿井瓦斯/气体浓度/烟雾浓度报警设计&#xff08;原理图源代码仿真答辩论文答辩PPT&#xff09; 文章目录【毕业设计】30-基于单片机矿井瓦斯/气体浓度/烟雾浓度报警设计&#xff08;原理图源代码仿真答辩论文答辩PPT&#xff09;任务书设计说明书摘要…

Kafka(二)- Kafka集群部署

文章目录一、安装部署1. 集群规划2. 虚拟机前置准备工作&#xff08;1&#xff09;配置IP&#xff08;2&#xff09;修改主机名称和hosts文件&#xff08;3&#xff09;关闭防火墙&#xff0c;关闭防火墙开机自启&#xff08;4&#xff09;克隆虚拟机3. 集群部署&#xff08;1&…

Oracle中ALTER TABLE的五种用法(三)

首发微信公众号&#xff1a;SQL数据库运维 原文链接&#xff1a;https://mp.weixin.qq.com/s?__bizMzI1NTQyNzg3MQ&mid2247485212&idx1&sn450e9e94fa709b5eeff0de371c62072b&chksmea37536cdd40da7a94e165ce4b4c6e70fb1360d51bed4b3566eee438b587fa231315d0a5a…

BP神经网络PID从Simulink仿真到PLC控制实现(含博途PLC完整SCL源代码)

单神经元自适应PID控制博途PLC完整源代码,请参看下面的文章链接: 博途PLC单神经元自适应PID控制_RXXW_Dor的博客-CSDN博客_单神经元pid控制1、单神经元作为构成神经网络的基本单位,具有自学习和自适应能力,且结构简单易于计算,传统的PID具有结构简单、调整方便和参数整定…