Unity学习之UI优化总结
一、UI动静分离
1. 什么是UI动静分离?
“动”指的是元素移动,或放大/缩小频率比较高的 UI。“静”就是静止不动的 UI,准确的说是界面上不会移动、旋转、缩放、更换贴图和颜色的 UI。在 Unity 中,UI 的 "动静分离" 是一种优化 UI 性能的重要策略,核心思想是将静态 UI 元素(不常变化的部分)与动态 UI 元素(频繁更新的部分)进行分离管理,以减少不必要的 UI 重绘和计算开销。
2. 为什么要将他们分离开来呢?
UGUI 和 NGUI一样,都是用网格模型构建 UI 画面的,在构建后都做了合并网格。合并之后,无论哪个 UI 元素变化就需要重新合并网格,原本不需要重新构建的内容也会一并重构。
Unity 的 UI 系统(UGUI)基于 Canvas 渲染,当 Canvas 下的任何元素发生变化时:
(1) 整个 Canvas 会被标记为 "脏"(Dirty)
(2) 系统会重新计算该 Canvas 下所有元素的布局(Layout Rebuild)
(3) 重新生成网格(Mesh Rebuild)并提交渲染
如果动静元素混合在同一 Canvas 中:
(1) 动态元素的频繁更新会导致静态元素也被频繁重新计算和渲染。
(2) 造成大量不必要的性能消耗,尤其在复杂 UI 场景中
3. 如何分离他们呢?
UGUI 和 NGUI都有自己的重绘合并节点,我们可以称它们为画板,UGUI是 Canvas,NGUI是 UIPanel。
以画板为节点进行拆分。把会动的 UI 元素放入专门为它们准备的合并用的画板上,再将静止不动的 UI 留在原来的合并节点上。(动静分离,将不变的物体放一个canvas下, 变化的物体放一个canvas下,优化合并时候的开销)
将静态元素和动态元素放在不同的 Canvas 中,这是动静分离的基础。
(1) 创建两个 Canvas:StaticCanvas
(静态 UI)和DynamicCanvas
(动态 UI)
(2) 静态元素放入StaticCanvas
,动态元素放入DynamicCanvas
二、UI字体拆分
1. Unity UI 字体优化:基于 TextMeshPro (TMP) 的字体拆分与常用字提取
在 Unity 项目开发中,UI 字体优化是提升性能(尤其是内存占用和渲染效率)的关键环节之一。传统的 Text
组件因采用位图字体渲染逻辑,易产生冗余纹理、顶点数过高及多语言适配困难等问题。TextMeshPro (TMP) 作为 Unity 官方推荐的 UI 文本解决方案,凭借矢量字算法、高效图集管理和灵活的字体配置,成为字体优化的核心工具。其中,字体拆分与常用字提取是基于 TMP 实现精细化字体优化的核心手段,下文将详细解析其原理、流程与实践价值。
2. TMP 与传统 Text 组件的核心差异
TMP 并非简单替代 Text,而是从底层渲染逻辑上进行了重构,其优势直接服务于性能优化目标:
(1) 渲染算法:Text组件位图字体渲染,依赖预生成的字体纹理; TextMeshPro (TMP) 矢量字算法,通过几何网格实时渲染字形
(2) 纹理管理:Text组件单个字体文件对应单一纹理,冗余字符占用大量空间;TextMeshPro (TMP) 动态生成字体图集 (Font Atlas),仅包含用到的字符
(3) 顶点数量:Text组件每个字符对应独立四边形,顶点数随字符数线性增加(1 个字符 = 4 个顶点);TextMeshPro (TMP) 合并相似字形的网格,顶点数仅为传统 Text 的 1/5~1/3
(4) 多语言适配:Text组件不同语言需单独导入字体文件,易产生纹理冗余,难以同屏显示; TextMeshPro (TMP) 基于字体同源(Font Asset)管理,支持多语言字符动态加载与同屏渲染
(5) 细节表现:Text组件无抗锯齿或依赖纹理分辨率,放大易模糊; TextMeshPro (TMP) 支持动态抗锯齿、字距调整、渐变等,清晰度不依赖纹理分辨率
3. TMP 字体优化的核心矛盾
TMP 虽大幅优化了渲染效率,但仍面临关键问题:字体文件(尤其是中、日、韩等表意文字)包含数万甚至数十万个字符,若全量加载会导致字体图集过大,占用过多内存。例如,一个包含完整汉字的 TTF 字体文件约 10MB~50MB,生成的图集可能达到数百 KB 甚至数 MB,而实际项目中 UI 用到的字符往往仅数千个。
因此,字体拆分(按场景 / 功能拆分字体资产)与常用字提取(仅保留项目实际用到的字符)成为 TMP 字体优化的核心方向 —— 本质是通过「裁剪冗余字符」减少图集大小,实现内存与性能的平衡。
4. 优化核心手段一:常用字提取(字符集裁剪)
「常用字提取」是指从完整字体文件中筛选出项目实际用到的字符,生成仅包含这些字符的「精简版 TMP 字体资产 (Font Asset)」,从根源上减少图集的字符数量,降低纹理内存占用。
(1) 步骤 1:收集项目中的所有 UI 字符
首先需明确项目中所有 UI 文本用到的字符(避免遗漏导致字体缺失显示「□」),常用收集方式有两种:
手动收集:适用于小型项目,直接从 TextMeshPro - Text UI
组件的「Text」字段中复制所有字符,汇总到一个 .txt
文件中(需去重)。
自动收集:适用于中大型项目,通过编写 Editor 工具遍历场景和预制体中的所有 TMP 文本组件,提取字符并自动去重。
using UnityEditor;
using UnityEngine;
using TMPro;
using System.Collections.Generic;public class TMProCharCollector : EditorWindow
{private HashSet<char> _allChars = new HashSet<char>();[MenuItem("Tools/TMP 字符收集")]public static void ShowWindow() => GetWindow<TMProCharCollector>("TMP 字符收集");private void OnGUI(){if (GUILayout.Button("收集所有场景/预制体中的 TMP 字符")){_allChars.Clear();// 1. 收集场景中的 TMP 文本CollectFromObjects(FindObjectsOfType<TMP_Text>());// 2. 收集预制体中的 TMP 文本(需根据项目路径调整)string[] prefabPaths = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Prefabs/UI" });foreach (string path in prefabPaths){GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(path));CollectFromObjects(prefab.GetComponentsInChildren<TMP_Text>(true));}// 3. 导出字符到 TXTstring charText = new string(_allChars.ToArray());System.IO.File.WriteAllText(Application.dataPath + "/TMP_CollectedChars.txt", charText);Debug.Log($"收集完成,共 {_allChars.Count} 个字符,已导出到 TMP_CollectedChars.txt");}}private void CollectFromObjects(TMP_Text[] texts){foreach (var text in texts){if (string.IsNullOrEmpty(text.text)) continue;foreach (char c in text.text) _allChars.Add(c);}}
}
(2) 步骤 2:生成精简版 TMP Font Asset
TMP 提供了官方工具「Font Asset Creator」,可基于收集的字符集生成仅包含目标字符的字体资产:
a. 导入原始字体文件(如 .ttf
、.otf
,放入 Assets/Fonts
目录);
b. 在 Project 窗口右键原始字体文件 → Create → TextMeshPro → Font Asset
,打开「Font Asset Creator」窗口;
c. 在窗口顶部设置基础参数(如字体大小、图集分辨率),关键步骤是导入字符集:
点击「Character Set」下拉框,选择「Custom Characters」;
点击「Load」按钮,选择步骤 1 生成的 TMP_CollectedChars.txt
,工具会自动加载所有目标字符;
d. 点击「Generate Font Atlas」生成图集,再点击「Save」保存为 TMP Font Asset(后缀为 .asset
)。
(3) 验证与补充字符
生成后需在场景中测试所有 UI 文本,若出现「□」(缺失字符),需将缺失字符补充到 TMP_CollectedChars.txt
中,重新生成 Font Asset。
5. 优化核心手段二:字体拆分(按场景 / 功能拆分 Font Asset)
「字体拆分」是指根据项目的场景划分或功能模块,将字体资产拆分为多个独立的 Font Asset(如「登录场景字体」「主界面字体」「背包系统字体」),而非全项目共用一个大字体资产。其核心目的是「按需加载 / 卸载」,减少运行时的内存占用。
6. 进阶优化:TMP 图集与内存管理
字体拆分和常用字提取的核心是优化图集,而 TMP 自身的图集管理机制也可进一步挖掘优化空间。
图集打包策略:动态图集 vs 静态图集
TMP 支持两种图集生成模式,需根据项目需求选择:
静态图集
通过「Font Asset Creator」预生成图集,运行时直接加载。优点是性能稳定,无运行时图集生成开销;缺点是字符固定,新增字符需重新生成 Font Asset。适合字符固定的项目(如大部分手游)。
动态图集
运行时根据显示的字符动态生成 / 更新图集。优点是灵活,支持动态文本(如玩家输入的用户名);缺点是首次显示新字符时会有图集生成开销,且可能因图集频繁更新导致内存波动。适合含大量动态文本的项目(如聊天系统、UGC 内容)。
三、拆分过重的UI与UI预加载
1. UI 过重的典型特征
(1) 元素数量过多:单个界面包含数百甚至上千个 UI 元素(如复杂的背包系统、排行榜、技能树)
(2) 层级嵌套过深:UI 节点嵌套层级超过 8-10 层(UGUI 的 Canvas 渲染会递归计算矩阵,层级过深导致 CPU 开销剧增)
(3) 纹理资源庞大:界面中使用大量高分辨率图集(如 4096x4096 以上),或未进行图集合并的零散图片
(4) 组件过度复杂:包含大量动画、粒子效果、mask 遮罩或复杂的布局组件(如 GridLayoutGroup 配合大量子元素)
(5) 加载耗时过长:界面从开始加载到完全显示的时间超过 100ms(人眼可感知的卡顿阈值)
2. UI 过重带来的性能问题
(1) 加载卡顿:实例化大量 UI 元素时的瞬时 CPU 峰值导致帧率骤降
(2) 内存占用过高:未优化的纹理和对象池导致内存占用激增,可能引发频繁 GC
(3) 渲染压力大:过多的 Draw Call 和顶点数量,导致 GPU 负载过高
(4) 输入响应延迟:UI 事件系统需要处理大量元素,导致交互响应变慢
3. 拆分过重 UI 的核心策略
拆分过重 UI 的核心思想是 **"分而治之"**,将一个复杂界面按逻辑或功能拆分为多个独立模块,实现资源的按需加载和管理。
(1) 按功能模块拆分,这是最常用的拆分方式,将一个复杂界面按用户操作流程或功能区域拆分为独立模块。
(2) 按显示优先级拆分, 根据元素在界面中的显示优先级和加载时机进行拆分,优先加载用户首先看到的内容。
(3) 按渲染层级拆分, UGUI 中,不同 Canvas 会产生独立的 Draw Call。将不同渲染层级的 UI 元素拆分到多个 Canvas 中,可以减少单个 Canvas 的更新成本。
4. UI 预加载策略
预加载是指在用户需要之前提前加载 UI 资源,避免使用时的加载卡顿。但预加载并非越多越好,需要平衡内存占用和加载时机。
预加载的时机选择
(1) 场景切换时预加载, 在场景加载过程中,利用加载界面的时间预加载下一场景可能用到的 UI 资源。
(2) 后台空闲时预加载, 在玩家操作间隙(如战斗结束后、任务完成时),利用 CPU 空闲时间预加载后续可能用到的 UI。
(3) 优先级预加载, 为不同 UI 分配预加载优先级,优先加载高优先级的资源。
5. 预加载资源的管理
预加载的资源需要妥善管理,避免内存泄漏和资源浪费。
(1) 使用对象池管理实例, 预加载的 UI 实例应存入对象池,需要时从池取出,不需要时放回池中,避免频繁创建和销毁。
(2) 资源释放策略
场景切换时:释放当前场景专用的预加载 UI 资源
内存紧张时:根据优先级释放低优先级的预加载资源
定时清理:定期检查并释放长时间未使用的预加载资源
// 资源释放管理器
public class UIResourceManager : MonoBehaviour
{public float memoryThreshold = 800; // 内存阈值(MB)public float checkInterval = 30; // 检查间隔(秒)private float lastCheckTime;private void Update(){if (Time.time - lastCheckTime > checkInterval){lastCheckTime = Time.time;CheckMemoryUsage();}}private void CheckMemoryUsage(){float totalMemory = System.GC.GetTotalMemory(false) / (1024 * 1024); // MBif (totalMemory > memoryThreshold){Debug.LogWarning($"Memory usage high: {totalMemory}MB, releasing unused UI resources");ReleaseUnusedResources();}}private void ReleaseUnusedResources(){// 1. 清理对象池中长时间未使用的UIUIPool.Instance.CleanupUnused(300); // 清理5分钟未使用的// 2. 释放当前场景不需要的预加载资源string currentScene = SceneManager.GetActiveScene().name;ReleaseSceneSpecificResources(currentScene);// 3. 按优先级释放低优先级资源ReleaseLowPriorityResources();}private void ReleaseSceneSpecificResources(string currentScene){// 释放非当前场景的专用资源// 实际实现需根据项目的资源管理策略}private void ReleaseLowPriorityResources(){// 释放优先级低于一定值的资源// 实际实现需跟踪资源优先级}
}
四、ScrollView 优化
在 Unity 开发中,ScrollView 是 UI 系统中常用的组件,用于展示大量列表内容(如背包、背包、聊天记录等)。然而,当处理大量数据时,ScrollView 容易出现性能问题 —— 尤其是持续滚动时的合批网格重构和渲染裁剪开销,导致帧率下降和卡顿。
1. ScrollView 性能问题的根源
要优化 ScrollView,首先需要理解其性能瓶颈的产生机制
(1) 合批网格重构(Batch Rebuild)
UGUI 的渲染依赖于 Canvas 下的网格合并(Batching)。当 ScrollView 中的内容滚动时:
每个可见的 UI 元素(如 Item)会被频繁更新位置
位置变化会导致 UI 布局重新计算(Layout Rebuild)
大量元素同时变动会打破原有的网格合批,触发频繁的合批重建
每帧重建合批网格会产生显著的 CPU 开销,表现为滚动时帧率骤降
(2)渲染裁剪(Culling)的低效性
ScrollView 通过 Mask 组件实现可视区域裁剪,但默认实现存在缺陷:
Mask 组件会强制开启模板测试(Stencil Test),增加 GPU 负担
即使元素完全在可视区域外,仍可能参与布局计算和事件检测
大量不可见元素的存在会导致 Canvas 网格体积庞大,降低更新效率
(3)大量实例化 / 销毁的开销
未优化的 ScrollView 常采用 "一次性创建所有 Item" 的方式:
初始化时创建成百上千个 Item 预制体,导致初始加载卡顿
滚动时若动态创建 / 销毁 Item(如无限滚动),会触发频繁 GC
每个 Item 包含多个组件(Image、Text、Button 等),实例化成本高
2. 对象池(Object Pool)优化:核心解决方案
对象池技术是解决 ScrollView 性能问题的关键,其核心思想是复用已创建的 Item 实例,避免频繁创建和销毁,同时通过只保留可视区域内的 Item 减少合批和裁剪开销。
对象池优化的基本原理
预创建:在 ScrollView 初始化时,创建少量(略多于一屏可见数量)的 Item 实例,存入对象池
复用:滚动时,将离开可视区域的 Item 回收至对象池,需要显示新内容时从池中取出并更新数据
动态调整:根据滚动速度和方向,智能判断需要激活 / 回收的 Item,保持可视区域内的 Item 数量稳定
五、UI 贴图设置优化
UI 贴图是 Unity 项目中内存占用和渲染性能的关键影响因素之一。一张未经优化的 UI 贴图可能会占用大量内存,增加 Draw Call 数量,并导致渲染性能下降。
1. UI 贴图的常见问题与性能影响
(1) 内存占用过高:高分辨率、未压缩的贴图会消耗大量内存资源
(2) Draw Call 激增:零散的小贴图会导致无法有效合批,增加 Draw Call
(3)加载速度慢:大尺寸贴图会延长加载时间,导致卡顿
2. 贴图导入设置优化
(1)纹理类型(Texture Type)设置
对于 UI 元素,应将 Texture Type 设置为 Sprite (2D and UI),这会:启用 Sprite 特定的导入设置, 优化贴图用于 UI 渲染, 支持 Sprite Packer/Atlas 功能
(2) 贴图尺寸(Max Size)优化
UI 贴图的尺寸应遵循 2 的幂次方(Power of Two, POT) 原则,如 32、64、128、256、512、1024、2048 等。
根据实际显示大小设置合适的 Max Size,避免过大
移动端建议最大不超过 2048x2048
避免使用非 2 的幂次方尺寸,这会导致压缩效率降低和内存浪费
3. 压缩格式(Compression)选择
根据目标平台选择合适的压缩格式,平衡质量和性能
移动端:
Android:推荐使用 ETC1 或 ETC2 格式(ETC2 支持透明通道)
对于高质量 UI,可考虑 ASTC 格式(需 OpenGL ES 3.0+ 支持)小图标可使用 RGBA4444 格式
iOS:推荐使用 PVRTC 格式
A9 及以上设备可考虑 ASTC 格式
需要透明的 UI 元素建议使用 PVRTC 4 bits with alpha