Administrator
发布于 2025-06-04 / 2 阅读
0

Unity之UGUI优化教程

# Unity UGUI 优化教程

在使用 Unity UGUI(UI Toolkit 之前常用的内置 UI 系统)进行界面开发时,UI 性能往往是许多项目的瓶颈。本文将从多方面讲解 UGUI 的优化思路和最佳实践,帮助你在项目中实现流畅的 UI 体验。

---

## 目录

1. [引言与原理概述](#引言与原理概述)

2. [Canvas 批处理与重绘优化](#canvas-批处理与重绘优化)

1. [Canvas 的工作原理](#canvas-的工作原理)

2. [减少 Canvas 重建](#减少-canvas-重建)

3. [合理分割 Canvas](#合理分割-canvas)

3. [Draw Call 与批次合并](#draw-call-与批次合并)

1. [UI Sprite 打包与图集](#ui-sprite-打包与图集)

2. [Font Atlas 优化](#font-atlas-优化)

3. [动态图片与精灵处理](#动态图片与精灵处理)

4. [资源加载与内存管理](#资源加载与内存管理)

1. [避免 Runtime 生成过多纹理](#避免-runtime-生成过多纹理)

2. [Sprite Atlas 与 Addressable](#sprite-atlas-与-addressable)

3. [垃圾回收与内存泄漏](#垃圾回收与内存泄漏)

5. [UI 结构与布局优化](#ui-结构与布局优化)

1. [避免非必要的 Layout 组件](#避免非必要的-layout-组件)

2. [使用自定义 Vertex Effect vs. LayoutGroup](#使用自定义-vertex-effect-vs-layoutgroup)

3. [滚动列表与虚拟化](#滚动列表与虚拟化)

6. [动画与特效性能优化](#动画与特效性能优化)

1. [减少 Update/OnEnable 调用](#减少-updateonenable-调用)

2. [使用 CanvasGroup 与 CanvasRenderer](#使用-canvasgroup-与-canvasrenderer)

3. [Tween/Animator 优化建议](#tweenanimator-优化建议)

7. [脚本调用与事件优化](#脚本调用与事件优化)

1. [减少频繁 SetActive 与 SetDirty](#减少频繁-setactive-与-setdirty)

2. [避免频繁使用 Find / GetComponent](#避免频繁使用-find--getcomponent)

3. [事件订阅与解绑](#事件订阅与解绑)

8. [测试与诊断工具](#测试与诊断工具)

1. [Profiler 常见指标解读](#profiler-常见指标解读)

2. [UIStats / Frame Debugger](#uistats--frame-debugger)

3. [第三方插件推荐](#第三方插件推荐)

9. [总结与最佳实践清单](#总结与最佳实践清单)

---

## 引言与原理概述

Unity UGUI(从 Unity 4.6 开始集成的 UI 系统)易于上手、功能灵活,但在复杂的 UI 场景下,若不加以优化,容易出现界面卡顿、帧率下降、内存占用过高等问题。了解其内部渲染和重绘原理是优化的前提。

- 底层渲染流水线

1. Canvas 重建(Rebuild):当 UI 属性(尺寸、文本、颜色等)发生变化时,CGUI 会标记整个 Canvas 或部分区域为 “Dirty”,触发重建过程。

2. 顶点缓冲:UGUI 会为每个可见的 UI 元素(Image、Text、Mask 等)生成顶点数据,通过 CanvasRenderer 合并到一个或多个 Vertex Buffer。

3. 合批(Batch):相同材质(同一材质、同一纹理、同一渲染设置)的 UI 元素会合并到同一次 Draw Call 中。

4. 绘制(Draw):GPU 端执行具体的三角形绘制。

- 性能瓶颈常见点

- 频繁重建 Canvas:修改 UI 属性时,如果不加控制,会导致多次重建。

- 过多 Draw Call:不同材质或未合并到图集的 Sprite、Text,会生成单独 Draw Call。

- 复杂布局与布局组件:Grid Layout、Content Size Fitter、LayoutGroup 等会在每帧重新计算布局。

- 动态创建/销毁:Instantiate/Destroy 大量 UI 对象会触发垃圾回收与重绘。

以下章节将针对上述瓶颈,给出具体优化方法。

---

## Canvas 批处理与重绘优化

### Canvas 的工作原理

- 每个 Canvas 对象会维护一个 “Dirty” 状态。

- 当子元素发生以下变化时,Canvas 会被标记为 Dirty:

- 位置、缩放、旋转(RectTransform)

- 颜色、透明度(Image、Text、CanvasGroup)

- 文本内容、字体样式(Text 组件)

- Mask、Clipping 区域变化

- Dirty 标记级别

- Canvas.Rebuild:整体 Canvas 重建,包括顶点数据重新生成和合批。

- Layout Rebuild:LayoutGroup/ContentSizeFitter 重新计算布局。

- Graphic Rebuild:单个 UI 元素的顶点重生。

> 小提示:调用 Canvas.ForceUpdateCanvases() 可以强制立即执行重建,但不建议在运行时频繁调用,除非调试或非常特殊场景。

### 减少 Canvas 重建

1. 避免频繁修改 RectTransform

- 如果需要移动多个子元素,先将父物体整组移动,而非分别修改子节点。

- 使用 RectTransform.anchoredPosition3D = newPos; 而非修改 transform.position,避免切换 3D/2D 执行路径。

2. 合并批量属性修改

- 将对同一个 Canvas 的多次属性修改合并到一帧内完成,而不是拆分到多帧,减少多次重建。

- 通过开启或关闭 Canvas 的交互时段,使中间状态不触发重绘:

```csharp

Canvas.ForceUpdateCanvases(); // 可在最后一次修改后调用,强制一次重建

```

3. 禁用 / 激活 CanvasGroup

- 如果短时间内需要隐藏/显示大量 UI 元素,可以考虑使用 CanvasGroup.alphaCanvasGroup.interactable = false; ,而不是频繁 SetActive(false),减少 Canvas 重建次数。

4. 注意 Layout 组件触发重建

- LayoutGroupContentSizeFitterAspectRatioFitter 等会在元素尺寸或子元素增减时触发布局计算与重绘。

- 若界面中包含多个 LayoutGroup,可在批量改动子元素时,先禁用 LayoutGroup:

```csharp

layoutGroup.enabled = false;

// 批量增删子元素

layoutGroup.enabled = true;

LayoutRebuilder.ForceRebuildLayoutImmediate(layoutGroup.GetComponent<RectTransform>());

```

### 合理分割 Canvas

- 尽量减少单个大 Canvas

- 将界面划分为多个小 Canvas,例如:

1. 顶层 UI(整局面板、Overlay)

2. 字符血条 / 动态头像

3. 对话框或弹窗

- 不同模块使用独立 Canvas,可避免一个子组件变化导致整个大 Canvas 重建。

- 静态与动态分离

- 静态不变或少变化的 UI 元素(背景图标、固定按钮)放到一个 Canvas。

- 高频率变化的部分(血条、文字提示)放到另一个 Canvas。

- 这样,动态区域的修改不会影响静态区域的重建。

- 使用 Overlay/Camera 渲染模式区分

- UGUI 支持三种渲染模式:

1. Screen Space – Overlay:直接覆盖屏幕。

2. Screen Space – Camera:由指定相机渲染。

3. World Space:位于世界坐标,受摄像机影响。

- 对于与 3D 场景深度相关的 UI,比如角色头顶血条,使用 World Space Canvas;而 HUD、固定面板用 Overlay。

---

## Draw Call 与批次合并

### UI Sprite 打包与图集

- 问题:如果多个 Image 组件使用不同 Texture 或者相同 Texture 但未打包到同一个 Atlas,则会产生额外的 Draw Call。

- 解决:使用 Sprite Atlas(2D 图集)或第三方插件(如 TexturePacker、SVNTexturePacker)将小图标、按钮等打包到一个大图集中。

#### 方法一:Unity 内置 Sprite Atlas

1. 创建 Sprite Atlas

- 在 Project 面板右键 → Create → 2D → Sprite Atlas,将常用的 UI 图标、按钮、进度条等拖入 “Objects for Packing”。

- 在 Inspector 中勾选 “Include in Build” 并设置合适的分辨率、打包格式(RGBA 32bits、ETC2 等)。

2. 引用 Atlas 内 Sprite

- 将 UI Image 的 Source Image 设置为打包好的 Sprite。

- Unity 会在运行时自动使用 Atlas 合并贴图,减少 Draw Call。

3. 动态扩展

- 若需要动态加载新 Sprite,可通过 SpriteAtlasManager API:

```csharp

using UnityEngine.U2D;

...

SpriteAtlas atlas;

bool loaded = SpriteAtlasManager.TryGetAtlas("MyAtlasName", out atlas);

if (loaded) {

Sprite sp = atlas.GetSprite("MySpriteName");

myImageComponent.sprite = sp;

}

```

#### 方法二:ZerO2D / TexturePacker 等第三方

- 优势:支持更灵活的平台贴图压缩、更细粒度控制。

- 注意:需保证每个场景或模块的 Sprite 资源提前打包,避免在加载时产生临时小图集。

### Font Atlas 优化

- 文本 Draw Call 问题:每个 Text 组件会使用同一个字体的 Font Atlas 生成顶点。如果字体类型不同或超过 Atlas 大小,会产生额外 Draw Call。

- 方法

1. 使用同一字体:全局统一使用同一种字体(或同一字体家族)。

2. Font Size 与 Dynamic Atlas 控制:对于 Dynamic Font,Unity 会动态生成字符 Glyph 到 Atlas。若字符集过大,会拆分成多个 Atlas,增加 Draw Call。

- 建议使用 TTF/OTF 字体文件,尽量在项目中预先描绘常用字符,避免运行时大量动态扩展。

- 在 Text 组件的 Inspector 中,调用 Font.RequestCharactersInTexture("常用字符", size, FontStyle.Normal) 预先生成字符。

```csharp

void PrewarmFont(Font font, int fontSize, string charset)

{

foreach (char ch in charset)

{

font.RequestCharactersInTexture(ch.ToString(), fontSize, FontStyle.Normal);

}

}