UI 性能优化(持续更新中)
性能优化
性能优化是个老生常谈的问题了,而且性能优化不是开发完再开始优化,是做之前就想好优化方向,在做的时候就要开始注意。
1 | A[Canvas] --> B(标记脏区域) |
减少Draw Calls
使用Atlas图集:将多个小的UI图像合并成一个大图集,减少纹理切换的次数,由于纹理是预先合并的,GPU在渲染时不需要频繁切换纹理,减少了状态切换的开销,从而提高了渲染效率。注意必须要合并到同一个图集的同一张中,如果因为图片太大导致在同一个图集的不同张中是无法减少 Draw Call 的。
所以背景图不建议放到图集中,图标和小的 UI 切图建议打成图集。
合理使用Canvas:将需要频繁更新的UI元素放在独立的Canvas中,以减少整个UI的重绘次数。
纹理格式
格式设置规则
根据平台对图片进行合适的设置。
优先使用压缩格式
除非特殊需求(如后期处理源纹理、遮罩图),始终优先选择平台特定压缩格式,可显著减少内存占用和GPU带宽。考虑Alpha通道
需要透明度时选择支持Alpha通道的格式(如RGBA)。根据纹理用途选择
用途 推荐格式 注意事项 UI/Sprite 高精度格式 对压缩伪影敏感 背景/贴图 高压缩率格式 容忍度较高 法线贴图 专用格式 需高精度存储方向信息 遮罩图 无压缩/低压缩 需8bit/通道精度 利用Platform Override
在Inspector窗口的纹理导入设置中,为不同平台单独设置格式。
Android
比如安卓平台
首选 (现代设备, API 21+):
ASTC
- 提供多种块尺寸:ASTC 4x4
/5x5
:高质量UI/角色精灵ASTC 6x6
/8x8
:通用背景/贴图ASTC 10x10
/12x12
:大型背景纹理
备选 (旧设备兼容):
ETC2
:ETC2 RGB 4 bits
:不透明纹理ETC2 RGBA 8 bits
:透明纹理
不推荐:
ETC1
(过时,不支持真Alpha)
安卓 Override ETC2 fallback
ASTC 是 Android 上推荐的现代纹理压缩格式(API Level 21+ / OpenGL ES 3.0+ 设备支持)。
Override ETC2 fallback 是一个针对 ASTC 压缩格式的兼容性选项。它的作用是控制 当目标 Android 设备不支持 ASTC 格式时,Unity 应该回退到哪种替代格式来加载纹理。
如果开启了该选项,当游戏运行在不支持 ASTC 的老设备上时,Unity 不会直接报错崩溃。Unity 会自动寻找或生成一个替代格式的纹理数据来使用。这个替代格式就是 ETC2(或其变体)。ETC2 是 OpenGL ES 3.0 标准的一部分,支持更广泛的设备(API Level 18+)。
如果不勾选此选项 (默认行为):Unity 会使用一个默认的回退格式。通常是 ETC2_RGBA8 (8 bits/pixel),这是一个质量相对较高的 ETC2 格式,支持完整的 RGBA 通道(包括透明度)。
如果勾选此选项:你可以手动选择当回退发生时使用哪种具体的 ETC2 (或其兼容格式):
选项的意义:
32-bit: 回退到 未压缩的 RGBA32。质量最高,但内存消耗巨大 (4 bytes/pixel)。非常不推荐,仅用于极端调试。
16-bit: 回退到 未压缩的 RGBA16 (通常是 4444 或 565 格式)。质量较低,内存消耗仍然较大 (2 bytes/pixel)。通常也不推荐。
32-bit(half-resolution): 使用 32-bit 但是分辨率减半
如果用 use build settings,打开 build 界面就可以查看到。
iOS/tvOS
首选:
ASTC
- 块尺寸策略同Android备选 (极老设备):
PVRTC
:PVRTC RGB 2/4 bits
:不透明PVRTC RGBA 2/4 bits
:透明
要求纹理为2的幂且接近正方形
Windows/macOS/Linux
首选:
BCn (DXTn)
系列:BC1 (DXT1)
:不透明纹理BC3 (DXT5)
:透明纹理BC7
:高质量RGBA(需DX11+)
特殊用途:
BC4 (ATI1)
:单通道数据BC5 (ATI2)
:法线贴图
备选:
RGBA 32 bit
:最高质量(慎用)RGBA 16 bit (4444)
:内存敏感场景
WebGL
首选:
ASTC
- 现代浏览器支持最佳备选:
ETC2
:WebGL 2.0标准支持DXT (BCn)
:需WEBGL_compressed_texture_s3tc
扩展
兜底:
RGBA 32 bit
- 当无压缩支持时回退
通道分离
分离 Alpha 通道:
如果纹理的 Alpha 通道(透明度)不需要高精度(例如仅用于遮罩),可以将其分离为更低精度的格式(如 8 位灰度图),而保留 RGB 通道为高质量格式(如 ETC2)。示例:
原始 RGBA32 纹理:4MB → 分离后(RGB24 + Alpha8):3MB + 1MB = 4MB(但可对 Alpha 进一步压缩)
使用压缩格式(如 ETC2_RGB + ETC2_Alpha):可显著减少内存(如从 4MB 降至 1MB)。
提升渲染性能
减少带宽占用:
在移动设备上,GPU 带宽是瓶颈。分离后可以仅加载需要的通道(例如只读取 Alpha 通道用于遮罩计算),减少数据传输量。Shader 优化:
在 Shader 中,如果只需要单通道数据(如金属度、粗糙度),直接读取分离后的灰度图比解码 RGB 纹理更高效。
实现特殊效果
通道复用:
将不同数据存储到同一纹理的多个通道中,减少纹理采样次数。例如:金属度(R) + 粗糙度(G) + 高度(B) 合并到一张纹理。
法线贴图的 XY 分量(RG),Z 分量通过计算还原。
遮罩控制:
分离 Alpha 通道用于动态遮罩(如角色溶解效果、地形混合)。
兼容性与平台适配
适配不同压缩格式:
某些平台(如 Android)对 Alpha 通道的支持有限(如 ETC1 不支持 Alpha)。通过分离 Alpha,可以单独处理(例如用 ETC1_RGB + 单独压缩的 Alpha 贴图)。跨平台优化:
针对不同平台选择最佳通道组合(如 iOS 用 PVRTC,Android 用 ETC2)。
工作流优化
- 减少纹理数量:
通过合并多张灰度图到一张 RGBA 纹理的各个通道(如 R=金属度,G=粗糙度,B=AO),减少纹理数量。
- 减少纹理数量:
通用设置建议
Generate Mip Maps
✅ 3D对象/会缩小的2D精灵
❌ 固定尺寸的UI纹理sRGB (Color Texture)
✅ 颜色纹理(漫反射/UI)
❌ 非颜色数据(法线贴图/遮罩图)Read/Write Enabled
❌ 默认关闭(开启会双倍内存)Filter Mode
Bilinear
:通用选择Point
:像素风游戏Trilinear
:需Mip Maps
Max Size
按实际显示尺寸设置,避免资源浪费
优化界面切换和组成逻辑
UI 动静分离
优化 UI 第一次打开和每次打开的物体显隐逻辑,不必要的 SetActive 的操作会导致多次出触发 Enable 或者 Disable 的操作,增加 CPU 的消耗,如果该物体上还有渲染相关的逻辑还会改变渲染状态,增加渲染引擎的额外工作。
优化显隐的逻辑可以用 Canvas Group 或者移动到远点的操作来代替。
需要注意的是,移动到远处可能会和 UI 动画相悖,需要做特殊处理。
如果一个界面下面有多个 Tab 页签,建议做动态加载,不要都放到预制体做显隐切换。
不要留存预制体中的遗留组成,比如从其他界面复制过来然后修改成新的界面时,留存的不必要的界面组成都删掉。
UI 中的常驻特效也不要放在预制体里面,会在打 Bundle 的时候造成冗余资源,动态加载。
降低UI更新频率
限制Update频率:对不需要每帧更新的UI元素,使用协程或定时器进行更新,减少CPU负担。
使用Event System:对于不需要每帧更新的交互,使用Unity的Event System来响应用户输入。
一些配合 Render Texture 使用的系统,比如显示 Spine 和 Live2D 动画的地方,也可以考虑分量更新,在同一个屏幕中如果有多个 RT,可以分别更新。
减少复杂的布局计算
- 简化Layout Group:尽量减少使用Layout Group(如Vertical Layout Group、Horizontal Layout Group)进行复杂布局,使用简单的Transform位置调整,建议动态 Scroll 都自己写一个,这样无论是无限的还是有限的 Group 都可以用坐标来计算。
- 缓存布局计算结果:对于静态或不频繁变化的UI元素,计算一次布局后缓存结果,避免重复计算。
ASTC 材质大小计算方式
假设你有一个2048x2048的纹理,选择ASTC 12x12块:
1 | 块宽度 = 12,块高度 = 12,每块字节数 = 128(对于12x12块) |
资源管理与预加载
- 合理管理UI资源:对不常用的UI资源进行动态加载与卸载,保持内存使用在合理范围内。
- 预加载关键UI:在进入某个场景前,预加载重要的UI资源,以减少加载时间。
输入检测
- 设置默认创建的 Image 组件 Raycast Target 为 False。
- 替换 Unity 默认的 Graphic Raycast。
对象池
- 设置全局的通用对象池和调用方法,不自己管理 GameObject 的生命周期。
优化二级界面
- 有的 UI 不属于全屏界面,但是效果却是全屏的效果,会增加 Draw Call,为这种情况增加自定义的优化方案,关闭二级界面后的全屏界面可以减少 Draw Call。
UI 分层
- 在 UI 分层时可以多考虑一些参数,同等优先级的 UI 下最好再增加一个排序参数。
Rebuild 和 ReBatching
特性 | Rebuild | Rebatching |
---|---|---|
发生层面 | C# 层(UGUI 逻辑) | C++ 层(底层渲染引擎) |
触发条件 | Layout变化、材质更新、Mask裁剪等 | 网格变化(显隐、位移、颜色、文本内容等) |
耗时关系 | 与动态节点数量线性相关 | 与节点总数和覆盖关系非线性相关 |
优化线程 | 单线程 | Unity 5.2+ 支持多线程 |
Rebuild:C#层的布局与材质更新
Rebuild 主要负责处理UI元素的逻辑状态变化,分为三部分:
Layout Rebuild
当LayoutGroup(如GridLayout、VerticalLayout)的直接子物体发生尺寸或位置变化时触发。
递归计算子元素的布局,耗时与变化的Graphic组件(Image/Text)数量成正比。
Graphic Rebuild
修改UI元素的属性(如Image.color、Text.text、Sprite替换)时触发。
关键机制:
修改Image.color会更新顶点色,触发网格重建(但材质不变,不增加DrawCall)。
修改Material或Shader属性(如_Color)会新增DrawCall,但不触发Rebatch。
3. Mask Rebuild
Mask或RectMask2D执行裁剪时,需更新被遮罩区域的顶点数据。
RectMask2D性能优于Mask,因其不生成新材质。
Rebatching:C++层的网格合批优化
Rebatching 的目标是将相同材质/贴图的UI元素合并为一个DrawCall,过程如下:
深度计算(Depth Calculation)
按Hierarchy顺序深度遍历,跳过非活跃节点。
规则:
- 无重叠 → Depth=0
- 有重叠 → Depth = 被覆盖节点的最大Depth +(材质不同?1 : 0)
排序与合批
按Depth、材质ID、贴图ID排序,合并连续且参数相同的节点。
合批中断条件:
- 中间插入不同材质的UI(如Text夹在两个同图集Image之间)。
- Z值不为0或层级非连续。
网格生成
- 将合批结果生成单一Mesh,提交给GPU。
- 性能瓶颈:Canvas内节点越多,贪心算法耗时越⻓(非线性增长)。
优化字体资源
使用自定义字体的时候,比如汉子可以不用所有的汉子,只需要常用的 3500-7000 个字即可。
随着编辑器的运行调试,字体资源的图集纹理也会越来越大,如果构建时不清除图集纹理,最终的包体中,图集纹理也会占用对应的空间,点击根据字体文件创建的 SDF 文件,在检视面板中勾选:Clear Dynamic Data on Build。
异步加载UI
使用异步加载减少卡顿
异步加载可以在不阻塞主线程的情况下加载资源,特别适合于加载大图片或复杂UI。
延迟初始化
对于不需要立即显示的UI元素,可以采用延迟初始化的策略,在需要时再进行加载和初始化。
ReckMask 替代 Mask
特性 | Mask (模板遮罩) | RectMask2D (矩形遮罩) |
---|---|---|
实现原理 | 模板缓冲区(Stencil Buffer) | 裁剪矩形(Scissor Rect) |
DrawCall影响 | 增加2个DrawCall | 不增加额外DrawCall |
合批影响 | 中断合批 | 保持合批 |
形状支持 | 任意形状 | 仅矩形 |
性能开销 | 高(模板操作+材质实例) | 低(硬件加速裁剪) |
旋转支持 | 完美支持旋转 | 旋转后仍为轴对齐矩形 |
嵌套支持 | 支持 | 支持 |
image和rawImage的区别
(sprite和texture)
特性 | Image (图像) | Raw Image (原始图像) |
---|---|---|
纹理类型 | 主要使用 Sprite (精灵) | 使用 Texture (纹理) |
UV 矩形 | 不支持 动态设置UV。整个Sprite被绘制。 | 支持 通过 uvRect 动态设置UV坐标,可以实现纹理滑动、裁剪、放大等效果。 |
九宫格拉伸 | 支持 Sprite 的九宫格(Border)设置,实现高质量的可伸缩UI元素(如按钮、面板)。 | 支持 九宫格拉伸。纹理会整体均匀拉伸,容易变形。 |
性能优化 | 通常性能更优。多个使用相同Sprite和Material的Image可以被合批(Batching)。 | 性能开销可能稍大。如果使用独特的Texture(如Render Texture),可能会打断合批。 |
常用场景 | UI图标、按钮、背景面板等标准UI元素。 | 显示游戏内实时渲染的画面(如小地图、监控屏)、视频播放、动态生成的纹理、复杂的动态UI背景。 |
底层原理的区别
两者的根本区别在于它们的默认材质(Material) 和传递给着色器(Shader)的数据。
网格生成与顶点数据
Image 组件:
- 它根据 Sprite 的网格类型(如Full Rect、Tight)来生成一个四边形网格(Mesh)。
- 如果启用了 Preserve Aspect(保持宽高比),它会调整网格的顶点位置来保持图片比例。
- 最关键的是,它会将 Sprite 的UV坐标传递给着色器。这些UV坐标已经包含了图集(Atlas)中的位置信息(如果你使用了Sprite Atlas)。
- 它使用 UnityUI 默认的着色器,该着色器支持像素抓取(Pixel Snap)、九宫格拉伸等特性。
Raw Image 组件:
- 它总是生成一个简单的四边形网格(两个三角形),不考虑任何精灵的复杂网格类型。
- 它将一个简单的 [0, 1] 范围的UV坐标传递给着色器。
- 你可以通过 uvRect 属性来修改这个UV范围。例如,设置 uvRect 为 (0, 0, 0.5, 0.5) 意味着只显示纹理的左下四分之一。这就是它能实现纹理滑动的根本原因。
- 它使用一个更简单、更通用的UI着色器,不支持九宫格拉伸。
材质与着色器 (Shader)
这是最核心的底层区别。你可以在Unity编辑器的 UI 目录下找到它们的默认Shader。
Image 的默认Shader: UI/Default 或 UI/Default (Overlay)
- 这个Shader内部有处理 Sprites/Default 类型的代码。
- 它有一个 _StencilComp, _Stencil, 等用于遮罩的属性。
- 它有一个 _TextureSampleAdd 变量。这个变量是为了处理图集(Atlas)的Alpha预乘问题。当Sprite位于图集中时,这个值由引擎设置,以确保颜色被正确采样。
- 它包含处理九宫格拉伸的逻辑。
Raw Image 的默认Shader: UI/Default
- 它使用一个更通用的 Unlit/Transparent 风格的着色器。
- 它没有 _TextureSampleAdd 这个变量,因为它假设你使用的纹理(如Render Texture、Video Texture)是独立的,不需要处理图集带来的Alpha预乘问题。
- 它没有九宫格拉伸的逻辑。
正因为这个Shader上的区别,如果你错误地将一个 Sprite 赋值给 Raw Image,可能会出现颜色异常(通常是变亮),因为它没有用 _TextureSampleAdd 来校正从图集中采样的颜色。反之,将 Texture 赋给 Image 组件则根本行不通。
合批 (Batching)
Image: 如果多个 Image 组件使用同一个 Sprite(来自同一个图集)和同一个 Material,Unity可以将它们动态合批为一个Draw Call,极大提升性能。
Raw Image: 如果每个 Raw Image 都使用一个独特的 Texture(比如每个玩家都有一个独特的 Render Texture 小地图),那么每个 Raw Image 几乎都会产生一个独立的 Draw Call,因为材质属性(主要是纹理)不同,无法合批。这是使用 Raw Image 时需要特别注意的性能开销点。