1 UI性能瓶颈

  • CPU

    • Canvas重建频繁:Canvas是Unity中用于承载和管理所有UI元素的根对象。每个UI元素必须附加在一个Canvas上,Canvas重建是指当Canvas上的UI元素发生变化时,整个Canvas需要重新绘制,频繁的重建会影响性能。

    • Draw Call过多:Draw Call实际上就是一次CPU向GPU发起的图形渲染接口的调用,每个UI元素都会产生一个Draw Call 。过多的Draw Call会增加渲染开销,降低性能,导致渲染开销过大。

    • 复杂的UI逻辑:复杂的UI逻辑和动画也会增加CPU负担。
    • 内存使用不当:大量图片、字体等资源的加载和未优化的内存管理会导致内存占用过高。
  • GPU

    • overdraw:指的是渲染过程中,像素被绘制多次的现象。

2 CPU性能优化

2.1 Rebuild

2.1.1 Rebuild概念

Rebuild指的是在Canvas组件中,UI元素需要重新计算、布局或渲染时所进行的操作。

UGUI里有几个关键的Rebuild类型:

  • Layout Rebuild(布局重建)
    • 当UI元素的布局属性(例如尺寸、锚点、对齐方式)发生变化时,Layout Rebuild会重新计算UI元素的布局。
    • 触发时机:调整RectTransform的属性、添加或删除子元素、更改布局组件(如Grid Layout或Vertical Layout Group)等操作。
  • Graphic Rebuild(图形重建)
    • 当UI元素的视觉表现发生变化时,Graphic Rebuild会重新绘制UI元素的网格。例如,更新颜色、透明度、纹理或更改UI元素的材质时,会触发Graphic Rebuild。
    • 触发时机:更改UI元素的材质或纹理、调整颜色或透明度、文本内容变化等。
  • Canvas重建
    • 当Canvas组件的属性发生变化,或需要重新计算整个Canvas中的内容时,Canvas Rebuild会重新构建整个Canvas的渲染数据。这个过程涉及所有子UI元素的重新布局和绘制。
    • 触发时机:更改Canvas的属性,如Render Mode、Pixel Perfect选项,或动态添加/移除Canvas中的UI元素。

2.1.2 Rebuild的影响

  • 造成额外的性能开销
    • Rebuild会增加CPU的计算量,如果频繁触发Rebuild,可能会导致性能问题,如帧率下降。
  • 增加Draw Call
    • 如果是因为材质、纹理等属性的更改导致的Rebuild,那么就会伴随着Draw Call的增加。

2.1.3 如何减少Rebuild

  • 避免频繁更改UI属性
    • 例如RectTransform、UI元素内容(如文本、图片)。
    • 用修改Scale为0代替SetActive(false),修改Scale为0不会导致rebuild,因为修改Scale只是改变了“渲染尺寸”,并没有改变RectTransform的实际尺寸和位置。
  • Canvas分层
    • 分离静态和动态UI元素,将频繁变化的UI元素放在一个单独的Canvas中,而静态元素放在另一个Canvas中。这样,当动态元素发生变化时,只会触发对应Canvas的Rebuild,而不会影响整个界面。
    • 避免过度分层:虽然将不同元素分层是有益的,但过多的Canvas会增加管理成本,因此在设计时应找到平衡点,避免过度分层。

2.2 DrawCall

2.2.1 为什么需要合批

UGUI中的每个组件都是由网格,材质球,贴图组成(可以将其看成扁的3D物体)。每创建一个组件,就会构建一个网格(从Canvas上的CanvasRender获取),然后将材质球与这个网格绑定,并且将贴图赋值给材质球。上述所有的操作都是在Canvas中完成的,因此我们的组件是依托于Canvas才能实现。而Canvas的渲染是在透明渲染队列里的,也就是说,我们UI组件的渲染是在Canvas中从后向前渲染的,并且可以产生透明的效果。试想一下,如果我们需要在Canvas上,显示n个组件,那我们就要生成n个网格,准备n个材质球,准备n张图片。调用n个DrawCall。显然,这样是不现实的,不仅会占用我们很多内存,过多的DrawCall还会造成卡顿,影响性能。因此我们需要进行合批的操作。

2.2.2 合批

合批指将多个UI元素的渲染操作合并成一个Draw Call,以减少CPU与GPU之间的指令传输,从而提高渲染性能。合批的主要条件包括相同的材质、纹理、渲染顺序以及在同一个Canvas中等。

注意:在我们合批完成后,就会形成一个静止的模型,一旦我们改变了这个模型的任何数据(包括控件的位置颜色,控件销毁,材质球参数等),Unity就会销毁这个静止的模型,重新生成一个新的。如果我们每时每刻都在移动某个控件,那就会不断地合批、销毁,再合批,再销毁…非常影响性能。

UI一般采用动态批处理,是因为UI元素通常具有动态性和变化频繁的特性,动态批处理能够在运行时灵活处理这些变化,显著减少Draw Call,提高渲染性能。

Rebuild和合批的关系

Rebuild是Canvas系统更新UI元素布局和渲染数据的过程,通常在UI元素发生变化时触发。

合批是在Rebuild完成后,Unity根据UI元素的状态尝试将多个渲染操作合并成一个Draw Call的过程。

2.2.3 合批条件

  • 相同材质和纹理
  • 相同渲染顺序
  • 相同Canvas

2.2.4 优化要点

  • 在同一界面的UI尽量使用相同的材质球,并且UI使用的所有Sprite图片打成一个图集。(Sprite Atlas图集是一种技术,它可以将多个Sprite合并成一个Sprite Atlas来减少Draw call数量。通常情况下,我们会把一整个界面放到同一个图集里面,而那些公共的图标一类的素材会放在公共图集下面。这样有助于合批和内存管理。)
  • 避免UI控件彼此覆盖。防止原本可以合批的UI,因为覆盖改变排序顺序,造成无法合批,增加drawcall。
  • 避免使用outline和shadow组件。这些组件原理是复制4份一样的文本在底层显示(此举不增加drawcall,但是增加了渲染的图形)

2.3 内存

2.3.1 优化图片资源

UI中的图片资源通常占用大量内存,因此优化图片资源是内存优化的关键。

  • 压缩纹理:使用Unity内置的纹理压缩(Texture Compression,将图片资源进行压缩以减少内存占用和加载时间。Unity支持多种纹理压缩格式,如DXT、PVRTC等)工具,可以显著减少纹理占用的内存。
  • 合理的图片尺寸:确保使用的图片尺寸与显示尺寸相匹配,避免使用过大的图片。

2.3.2 优化字体资源

字体的渲染也会占用大量内存,特别是在使用动态字体时。

  • 使用静态字体:尽量使用静态字体替代动态字体,静态字体只加载所需的字符集。
  • 字体缓存:通过缓存字体纹理,减少字体的重建开销。

3 GPU性能优化

GPU端的性能开销主要体现在OverDraw上。

Overdraw 是指在渲染场景时,多个像素在同一帧中被绘制多次的情况。这种情况在透明或半透明对象重叠时尤其常见,GPU需要对每一层都进行处理,导致重复计算。这种重复绘制会消耗额外的GPU资源,从而影响渲染性能。

成因

  • 透明物体的重叠: 在UI中,透明或半透明元素(如按钮、背景、阴影等)会叠加在一起。GPU必须按照从后到前的顺序依次渲染这些元素,每个被覆盖的像素都要重新计算多次,这导致Overdraw。
  • 无效的遮挡: 在一些场景中,某些UI元素会被其他元素完全遮挡,但由于渲染顺序的原因,这些被遮挡的元素仍然会被渲染,导致不必要的Overdraw。

优化

  • 减少透明度的使用
  • 避免不必要的重叠