# 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.alpha
或 CanvasGroup.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);
}
}