0.前言

想要对UGUI进行深度优化,  就必须要详细了解 UGUI 每个组件的详细细节  :

  1. https://www.drflower.top/posts/aad79bf1/
  2. https://learn.unity.com/tutorial/optimizing-unity-ui
  3. https://zhuanlan.zhihu.com/p/43111806
  4. https://zhuanlan.zhihu.com/p/343978391
  5. https://blog.csdn.net/cyf649669121/article/details/83142903#commentBox
  6. https://zhuanlan.zhihu.com/p/98359112
  7. https://zhuanlan.zhihu.com/p/339387759

 

Canvas

Canvas是一个Native层实现的Unity组件,被Unity渲染系统用于渲染分层几何体(layered geometry)在游戏世界空间中。
Canvas负责把它们包含的Mesh合批,生成合适的渲染命令发送给Unity图形系统。以上行为都是在Native C++代码中完成,我们称之为Rebatch或者Batch Build,当一个Canvas中包含的几何体需要Rebacth时,这个Canvas就会被标记为Dirty状态。

Canvas Renderer

几何体数据是通过Canvas Renderer组件被提交到Canvas中。

Sub Canvas

Canvas组件可以嵌套在另一个Canvas组件下,我们称为子Canvas,子Canvas可以把它的子物体与父Canvas分离,使得当子Canvas被标记为Dirty时,并不会强制让父Canvas也强制Rebuild,反之亦然。但在某些特殊情况下,使用子Canvas进行分离的方法可能会失效,例如当对父Canvas的更改导致子Canvas的大小发生变化时。

Graphic

Graphic是UGUI的C#库提供的一个基类。它是为Canvas提供可绘制几何图形的所有UGUI的C#类的基类。大多数Unity内置的继承Graphic的类都是通过继承一个叫MaskableGraphic的子类来实现,这使得他们可以通过IMaskable接口来被隐藏。Drawable类的子类主要是Image和Text,且UGUI已提供了同名组件。

Layout

Layout控制着RectTransform的大小和位置,通常用于创建复杂的布局,这些布局需要对其内容进行相对大小调整或相对位置调整。Layout仅依赖于RectTransforms,并且仅影响其关联RectTransforms的属性。这些Layout类不依赖于Graphic类,可以独立于UGUI的Graphic类之外使用。

CanvasUpdateRegistry

CanvasUpdateRegistry, Graphic 和 Layout 都依赖于 CanvasUpdateRegistry 类,它没有暴露在Unity编辑器的界面上。这个类追踪那些必须更新的Layout类和Graphic类,并在其关联的Canvas调用willRenderCanvases事件时根据需要触发更新。

Rebuild

Layout和Graphic的更新称为Rebuild。本文档后面将进一步详细讨论Rebuild过程。

渲染细节

Canvas中所有几何体的绘制都在一个透明队列中,这就意味着由UGUI制作的几何体始终开启Alpha Blend,所以从多边形栅格化的每个像素都将被采样,即使它是完全不透明的。在移动设备上,过高的Overdraw会急剧升高填充率(Fill Rate)以超过GPU的可承受能力。

Batch Build过程(Canvases)

Batch Build(Rebatch):Canvas把表示它UI元素的网格合并起来,并生成合适的渲染命令发送到Unity的图形管线中。这个过程的结果会被缓存起来复用,直到这个Canvas被标记为Dirty,当Canvas中任何一个网格发生变化时,就会被标记成Dirty状态。

Canvas的网格是从从那些Canvas下的CanvasRenderer组件中获取的,但不包括子Canvas。

批处理计算需要对网格进行深度排序,并检查网格是否重叠,材质是否共享等。这种操作是多线程的,因此它的性能在不同的CPU架构中通常会有很大的不同,尤其是在移动soc(通常很少有CPU核心)和现代桌面CPU(通常有4个或更多核心)之间。

Rebuild过程(Graphics)

Rebuild:指重新计算Graphic的布局和网格的过程,这个过程在CanvasUpdateRegistry中执行 (这是一个C#类,我们可以在Unity的Bitbucket上找到源码) 。

在CanvasUpdateRegistry中,最重要的方法是PerformUpdate。每当Canvas组件调用WillRenderCanvases事件时,就会调用此方法。此事件每帧调用一次。

PerformUpdate执行流程

1.被标记为Dirty状态的Layout通过ICanvasElement.Rebuild方法请求重建布局。

2.所有裁剪组件(例如Mask),对需要被裁剪的组件进行剔除。这在ClippingRegistry.Cull中执行。

3.被标记了Dirty状态的Graphic请求重建他们的图形元素。

对于Layout和Graphic的重建,过程分为多个部分。布局重建分为三个部分 (预布局、布局和后期布局) ,而图形重建分为两个部分(预渲染和后期预渲染).

Layout的Rebuild

要重新计算包含一个或多个Layout的适当位置和大小,必须以适当的层级顺序进行布局计算,因为在GameObject在Hierarchy中靠近根的Layout,很可能会影响嵌套其中的子物体的Layout的位置和大小,所以必须优先计算。

为了做到这一点,UGUI把被标记为Dirty状态的Layout按照它们在Hierarchy中的层级对它们进行排序。Hierarchy中较高的项(即父变换较少的项)将移动到列表的前面。

然后这些排序后的Layout列表会请求重建它们的布局,这过程是那些UI元素被Layout真正改变位置和大小的过程。

Graphic的Rebuild

当Graphic进行Rebuild时,UGUI将控制权转交给ICanvasElement接口的Rebuild方法。Graphic类实现了这个接口,并在Rebuild过程的预渲染阶段执行两个不同的重建步骤。

如果顶点数据已标记为Dirty状态(如组件的矩形变换更改大小时),则重建网格。

如果材质数据已标记为Dirty状态(如组件的材质或纹理发生改变时),则将更新附着的画布渲染器的材质。

Graphic的Rebuild不需要按特定顺序遍历Graphic组件列表,也不需要任何排序操作。

Rebatch和Rebuild的触发条件

Rebuild通常会引起Rebatch.

触发Rebatch的条件

当Canvas下有Mesh发生改变时,如:

  1. SetActive
  2. Transform属性变化
  3. Graphic的Color属性变化(改Mesh顶点色)
  4. Text文本内容变化
  5. Depth发生变化

触发Rebuild的条件

  1. Layout修改RectTransform部分影响布局的属性
  2. Graphic的Mesh或Material发生变化
  3. Mask裁剪内容变化

主要关注的性能指标

  1. Canvas.BuildBatch函数耗时
  2. Canvas.SendWillRenderCanvases函数耗时
  3. Draw Call
  4. Overdraw
  5. 堆内存

动静分离(Canvas.BuildBatch函数耗时优化)

基于Rebatch是以Canvas为单位,当Canvas下UI元素发生变化时,会引起整个Canvas的重构,其中会包括网格合并,网格重叠检测,层级排序等操作。对于同一个界面,我们可以再细分Canvas,把相对静态的、不会变动的UI放在一个Canvas里,而相对变化比较频繁的UI就放在另一个Canvas里,使得频繁变化的Canvas里只对自己的Canvas下的元素进行Rebatch,而节省掉另一个Canvas中不需要变化的元素的Rebatch计算。

只有同一个Canvas下的UI元素才有可能合批,在中间新增Canvas会打断合批,动静分离优化本质是DrawCall换重构耗时的权衡。

Rebatch是在Canvas.BuildBatch函数中进行,而在Unity 5.2版本后,已经对Canvas.BuildBatch做了优化,优化后使用子线程进行计算,已经很大程度缓解了主线程的压力,目前来说动静分离并没有那么需要关注了。

合批规则(Draw Call)

UGUI的合批除了要求相同贴图和材质外,还对Depth和Hierarchy的层级等有要求。规则比较复杂繁琐,且源码不放开,实际情况很难做到完全把控。

可以借助FrameDebugger实时调试合批情况。

合批范围

处于同一个Canvas下的(不包括子Canvas)UI元素才能进行合批,同时排除掉active=false,CanvasGroup.alpha=0的元素不参与。

Depth计算

  1. 不渲染的UI元素Depth为-1,UI下没有和其他UI相交时,该UI的Depth为0。
  2. 当前UI下面有一个UI与其相交,若两者贴图和材质相同时,它们Depth相同,否则上面的UI的Depth是下面UI的Depth+1。
  3. 当前UI下面与多个UI相交,假设处于下面的多个UI中,最高Depth是MaxDepth,如果下面这些UI的Depth全是MaxDepth,则此UI不能与下面的UI合批,如果只有一个是MaxDepth,且贴图材质相同,则可以合批。

这里的相交是用Mesh计算相交,不是指RectTransform大小相交.

合批过程

  1. Depth计算完后,依次根据Depth、material ID、texture ID、RendererOrder(即UI层级队列顺序,HierarchyOrder)排序(条件的优先级依次递减),剔除depth == -1的UI元素,得到Batch前的UI元素队列VisiableList。
  2. 对VisiableList中相邻且可以Batch(相同material和texture)的UI元素合并批次,然后再生成相应mesh数据进行绘制。

注意策略

节点Position.z不为0时,视为3D UI,其子物体全部不参与合批,且打断前后合批。

图集

不同的图集/材质会产生DrawCall,通过图集把多个图片合并到一张贴图里,让这些图片可以进行合批,以此减少DrawCall

打图集规则:

  1. 同个界面、同个功能的图在同一个图集
  2. 不要把重复的图打到不同的图集中
  3. 通用的,使用频率高的图,单独放在一个图集
  4. 尺寸过大的图尽量不打进图集

对于类似背包界面这种包含多个系统功能道具图标这类界面,可以考虑使用动态图集

Overdraw

Overdraw是指一帧当中,同一个像素被重复绘制的次数。Fill Rate(填充率)是指显卡每帧每秒能够渲染的像素数。在每帧绘制中,如果一个像素被反复绘制的次数越多,那么它占用的资源也必然更多。Overdraw与Fill Rate成正比,目前在移动设备上,FillRate的压力主要来自半透明物体。因为多数情况下,半透明物体需要开启 Alpha Blend 且关闭 ZTest和 ZWrite,同时如果我们绘制像 alpha=0 这种实际上不会产生效果的颜色上去,也同样有 Blend 操作,这是一种极大的浪费。

  1. 减少UI重叠层级,隐藏处于底下被完全覆盖的UI面板。
  2. 对于需要暂时隐藏的UI,不要直接把Color属性的Alpha值改为0,UGUI中这样设置后仍然会渲染,应该用CanvasGroup组件把Alpha值置零。
  3. 需要响应Raycast事件时,不要使用空Image,可以自定义组件继承自MaskableGraphic,重写OnPopulateMesh把网格清空,这样可以响应Raycast而又不需要绘制Mesh。
  4. 打开全屏界面,关闭场景摄像机。对于一些非全屏但覆盖率较高的界面,在对场景动态表现要求不高的情况下,可以记录下打开UI时的画面,作为UI背景,然后关掉场景摄像机。
  5. 裁掉无用区域,镂空,对于Sliced类型的Image可以看情况取消Fill Center。
  6. 慎用Mask组件和Outline、Shadow组件
  7. 尽量保持UI上的粒子特效简单,尽量使用序列帧实现。

Unity在Scene视图可以切换成Overdraw模式,观察填充率情况,根据UWA的提示,Editor下看到的不完全准确,需要真机测试。

Raycast (新版可忽略, 待验证 TODO…)

UGUI创建的所有可点击组件都会默认开启RaycastTarget,当进行点击操作时,会对所有开启RaycastTarget的组件进行遍历检测和排序,实际上大部分的组件是不需要响应点击事件的,对于这些组件我们应该取消RaycastTarget属性,最好的方式是监听组件创建,在创建时直接赋值为False,对于需要响应事件的组件再手动开启。

更多优化可以参考一下文章:

Unity UGUI优化:解决EventSystem耗时过长的问题 第一部分
Unity UGUI优化:解决EventSystem耗时过长的问题 第二部分
Unity UGUI优化:解决EventSystem耗时过长的问题 第三部分
Unity UGUI 点击性能优化

Mask 与 RectMask2D

一般情况下,对于规则形状裁剪就可以考虑使用RectMask2D代替Mask,在DrawCall和Overdraw上有直接优势。但结合一个界面多个遮罩共存,遮罩区域是否相交、遮罩间内容是否能合批、遮罩内UI元素数量等,还需要进一步权衡。

Mask

  • 裁剪依赖Image组件,可根据Image裁剪不规则形状。
  • 增加两个DrawCall
  • 增加两层Overdraw
  • Mask与内UI不能与外面的UI合批,但不同Mask内部的多个UI可以进行合批,但当Mask之间有相交时,会打断合批。

RectMask2D

  • 裁剪范围按照RectTransform的Rect大小,不依赖Image,不能裁剪不规则形状。
  • 不增加DrawCall
  • 不增加Overdraw
  • RectMask2D内UI不能与外面UI合批且多个RectMask2D之间也不能合批。
  • RectMask2D在SendWillRenderCanvases函数中每帧有计算裁剪区域的操作(非重建操作),当其里面的UI元素很多时,耗时也值得注意。参考UWA问答-RectMask2D 是不是会频繁触发 SendWillRenderCanvases

Other

  • 慎用自带组件Outlien和Shaow,都是通过重复绘制多个Mesh实现的,其中Showdow绘制为原文本Mesh的2倍,而Outline为5倍,对渲染面数、顶点数,BuildBatch和SendWillRenderCanvases的耗时,Overdraw都有影响。若对于某种字体每次出现都需要这两种效果,可以让美术同学直接把阴影和描边做到字体里。
  • 隐藏界面时,可用CanvasGroup.Alpha=0,或者从Camera渲染层级里移除并将Canvas设置enable=false等方法隐藏,代替SetActive。
  • Test组件的Best Fit属性若非必要不要使用,它会使字号随着文本框大小而自动适配,一方面是适配本身在调整文本框大小时有CPU耗时开销,另一方面每个字号下新生成的字都会在Font Texture上占用一个字的大小,容易导致Font Texture过大(这个类似图集,当Font Texture当前大小放不下时才会占用更多内存)。
  • UI的图片一般关闭Read/Write和MipMaps。
  • 尽量少使用Layout组件,会增加Canvas.SendWillRenderCanvases函数耗时,利用好RectTransform同样可实现简单布局。
  • 对于血条、飘字、小地图标记等频繁更新位置的UI,可尽量减低更新频率,如隔帧更新,并设定更新阈值,当位移大于一定数值时再赋值(一方面是位移小时可能表现上看不出位移,另一方面是就算是没有实际位移,重复赋相同的值,也会SetDirty触发重建),可减少BuildBatch耗时。
  • 界面上的特效不要直接挂在在UI的Prefab上,而是用动态加载的方式,有助于减小UI的Prefab体积以及加载时间。

 

1.动静分离

跟Canvas的渲染机制有关,即如果一个UI元素始终是静态的,那么画布则不会重新计算绘制,如果是动态的则不然,所以,这里建议将动态和静态的元素分开。静态的元素从属一个Canvas,而需要动态变动的元素从属另一个Canvas。

2.消除不必要的射线检测(Raycast)

默认情况下,  Canvas自带Graphic Raycaster组件,  这对于交互型的UI是必须的,  但如果你的UI元素并不需要交互,  那么每次点击都会触发判断,  这种情况下,  你就可以删除Graphic Raycaster组件以减少不必要的计算处理. 同样的,  各种其他UI元素,  如图片、文本等也有Raycast Target属性,  如果不需要射线检测(触屏、点击),  也可以一并关掉,  减少计算量.

3.避免使用Camera.main (2020.2+ 版本中已经进行了优化,避免了它较高的CPU耗时)

unity默认创建的相机为主相机 (tag 为MainCamera) ,  即main camera,  可以通过unity api:  Camera.main 来直接获取,  本质上,  它是每次通过获取tag来取得的: GameObject.FindGameObjectWithTag(“MainCamera”)

所以, 建议在awake函数中缓存一次, 避免多次查找(GetComponent和Find等方法是很耗时的操作).

回到UGUI这边,  当画布的渲染模式切换为 world space 时,  event camera 的值为 none,  如果不指定,  它会默认指向 main camera,  每帧将会产生 7-10 次的查找操作.

4.慎用布局组件

布局组件是非常好用的功能组件,可以非常方便的用于元素内容的快速对齐排版。尽管如此,但你要是了解它的底层操作原理,你以后使用就会慎重了。因为每个布局组件会将一个 GetComponents 调用添加到每个子布局元素的脏读中,从而严重降低了嵌套布局组的性能。

另外:unity scroll rect 的实现,也是一个 Layout Group .

5.合并UI对象

对象被重复销毁和创建的时候,我们会考虑用对象池来优化,但是这样会产生一个新问题:UI元素被重新设置,禁用和合并,这会导致Canvas不必要地变脏。

推荐优化方法:

6.如何隐藏画布

通常我们禁用一个物件,使用的 gameObject.SetActive(true),如果我们这样禁用画布canvas的话,会触发画布显示重建,同时还会调用 onDisable 和 onEnable,增加了计算量。
推荐方法:_canvas.enable = false;

7.使用 Tween 库而不是 Animator

有时候,UI会配合动画来做一些效果,但是应该避免空的动画,即使动画值没变,动画却依然会弄脏元素,导致canvas不断重建。所以 UGUI中不使用没有任何变化的空闲动画.

推荐方式:自己写补间动画,或者直接使用三方补间库如 dotween 等.

8.尽量用RectMask2D替代Mask组件

Mask组件用到最多是在ScrollView中,网上也有很多资料介绍Mask组件会产生额外的两个drawcall,而大部分时候Mask都是矩形的,所以其实可以使用RectMask2D组件来替换掉Mask组件,官方也比较推荐RectMask2D组件,但如果你的Mask组件不是矩形的那么就不能替换。

9.检查UI组件的 Position 和 Rotation

之前在检查一个UI合批失效的问题,明明打了图集,但就是不能合批,很是诧异,经过测试发现如果你的Image组件pos.z不为零或者rota.x或rota.y不为零,那么会被当作3DUI从而不参与合批,增加drawcall,而这些非零值有时往往是不需要甚至是,不注意被设置上的,但却影响了这个合批,很是坑人。所以需要用工具检查这种被设置错误的UI,酌情进行重置.

10.滥用不可见组件

之前在Profile手头项目的时候发现红米上一个奇怪的现象:战斗界面维持60fps没问题;进入UI界面之后瞬间掉到45fps,甚至有的复杂界面掉到30fps。但战斗场景的Tris/Verts比UI高不少。
通过工具很方便的就定位到了瓶颈在于FillRate爆了,最后发现新手教学部分用了很多“不可见”的Image作为交互响应的控件;但这些东西虽然画上去没有效果,依然占用了显卡资源,特别是有很多大块的区域…找到问题之后就解决起来很方便:实现一个只在逻辑上响应Raycast但是不参与绘制的组件即可,改完之后帧率瞬间正常。

这里顺便提一句,显卡资源消耗在没有到瓶颈的时候,大概是随着使用的增加正相关,但是到瓶颈之后很多时候是 “崩盘” 节奏.

11. Image 优化建议

1.空的 Image会造成一个 DC,并且会打断合批.创建的时候将Image 的 SourceImage 给赋值,并且不设置 RayCastTarget.

 

2.颜色渐变和可以用一个新的material控制,本质上是更改Tint属性,这样既能满足颜色渐变,又能避免网格频繁重建.

 

3.直接从创建的地方将 Text/Image/RawImage 这 3 种类型修改,设置 raycastTarget属性为 false,可以降低性能.

 

4.Empty4Raycast.cs 配合 button .使用的此脚本可以在有效接受点击事件的情况下,不增加DrawCall和打断合批,
只要点击区域,不要显示内容的。可以把空白透明Image替换成qiankanglai提供的Empty4Raycast.
https://blog.uwa4d.com/archives/fillrate.html

 

5.Image叠加层数越多,overdraw 显示的颜色越红,同一个像素在同一帧绘制的次数越多.
这时候如果 Image是Sliced类型,中间区域是完全透明(相当于9宫格的边框),那么尽量将 Fill Center的勾去掉,
勾选掉即减少 Image 的显示部位.会缓解overdraw.

 

6.如果一张图片有很多区域是空白的,默认情况下fill rate是矩形区域。PolygonImage。可以减少透明区域的fill。
https://blog.uwa4d.com/archives/fillrate.html
即减少 Image 的透明部位.会缓解overdraw.但是可能会使顶点/三角形变多.
内存是足够的,overdraw 的危害比较大一些,该用就用.

 

7.Image 原生自带的图片是使用 Unity 本身自带的图片,开发过程中不能使用Unity 本身自带的图片,必须由我们本身设计的图片才行.
在源码 MenuOptions.cs 的 GetStandardResources 方法中,已将其屏蔽.

 

8.一张图片有很多空白,使用 Use Sprite Mesh 会缓解overdraw,但是增加 Mesh 的线条,在 Scene 窗口中,使用 Wireframe 查看.
此方式已经和 TexturePacker 工具打出的图片区别没有太大了.
在一张图上的透明通道很多的情况下的区别是:
Unity 自带工具打出的顶点数少,overdraw 高一点,TexturePacker打出的顶点数多,overdraw 少一点.
可以使用TexturePacker导出单张图片,然后使用 SpriteAtlas 组合成图集,再使用 Use Sprite Mesh 即可,
此方式与直接使用TexturePacker插件效果一致.

 

本站所有文章、资源、如无特殊说明或标注互联网转载外, 均为本站以及特邀作者、注册会员、游客投稿原创发布. 所提供下载链接均为站外链接, 网站本身并不存储相关资源文件, 所有资源仅用于个人学习及研究使用, 请在24小时内删除, 切勿用于商业用途, 如产生法律纠纷本站概不负责, 任何个人或组织, 在未征得本站同意时, 禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台. 如若本站内容侵犯了原著者的合法权益, 请及时通知本站, 待核实后将在三个工作日内进行删除.