性能优化

性能优化是个老生常谈的问题了,而且性能优化不是开发完再开始优化,是做之前就想好优化方向,在做的时候就要开始注意。

TD
1
2
3
4
5
6
A[Canvas] --> B(标记脏区域)
B --> C[Canvas.BuildBatch]
C --> D[生成网格数据]
D --> E[合批处理]
E --> F[提交渲染指令]
F --> G[GPU渲染]

减少Draw Calls

  • 使用Atlas图集:将多个小的UI图像合并成一个大图集,减少纹理切换的次数,由于纹理是预先合并的,GPU在渲染时不需要频繁切换纹理,减少了状态切换的开销,从而提高了渲染效率。注意必须要合并到同一个图集的同一张中,如果因为图片太大导致在同一个图集的不同张中是无法减少 Draw Call 的。
    无效
    有效

  • 所以背景图不建议放到图集中,图标和小的 UI 切图建议打成图集。

  • 合理使用Canvas:将需要频繁更新的UI元素放在独立的Canvas中,以减少整个UI的重绘次数。

纹理格式

格式设置规则

根据平台对图片进行合适的设置。

  1. 优先使用压缩格式
    除非特殊需求(如后期处理源纹理、遮罩图),始终优先选择平台特定压缩格式,可显著减少内存占用和GPU带宽。

  2. 考虑Alpha通道
    需要透明度时选择支持Alpha通道的格式(如RGBA)。

  3. 根据纹理用途选择

    用途 推荐格式 注意事项
    UI/Sprite 高精度格式 对压缩伪影敏感
    背景/贴图 高压缩率格式 容忍度较高
    法线贴图 专用格式 需高精度存储方向信息
    遮罩图 无压缩/低压缩 需8bit/通道精度
  4. 利用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 界面就可以查看到。
build settings

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
2
3
块宽度 = 12,块高度 = 12,每块字节数 = 128(对于12x12块)
块数=(2048 / 12)×(2048 / 12) ≈ 171×171 = 29241
总大小=29241×128≈3,744,768 字节≈3.57 MB

资源管理与预加载

  • 合理管理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 时需要特别注意的性能开销点。