从以下几个方面进行 UI 优化:
- UI 动静分离
- 拆分 UI
- 预加载
- Alpha 分离
- 字体拆分
- 滚屏优化
- 网格重构优化
- UI 展示与关闭的优化
- 对象池的运用
- 贴图设置的优化
- 内存泄漏
- 针对高低端机型的优化
- 图集拼接的优化
- UI 业务逻辑中GC的优化
4.6.1 UI 动静分离
什么是UI动静分离?
“动”指的是元素移动,或放大/缩小频率比较高的 UI。“静”就是静止不动的 UI,准确的说是界面上不会移动、旋转、缩放、更换贴图和颜色的 UI。
为什么要将他们分离开来呢?
UGUI 和 NGUI一样,都是用网格模型构建 UI 画面的,在构建后都做了合并网格。
合并之后,无论哪个 UI 元素变化就需要重新合并网格,原本不需要重新构建的内容也会一并重构。
如何分离他们呢?
UGUI 和 NGUI都有自己的重绘合并节点,我们可以称它们为画板,UGUI是 Canvas,NGUI是 UIPanel。
**以画板为节点进行拆分。**把会动的 UI 元素放入专门为它们准备的合并用的画板上,再将静止不动的 UI 留在原来的合并节点上。
4.6.2 拆分过重的 UI
为什么要拆分过重的 UI?
一个 UI 里的东西过多,在实例化和初始化时,消耗的 CPU 也会很大。
如何拆分?
把隐藏的 UI 界面拆分出来,使其成为独立运作的界面,只在需要它们时才调用并实例化。如果在拆分后界面内容依然很多,则要二次拆分,即把二次现实的内容进一步拆分。
**二次显示的内容:**打个比方,一个界面打开时会显示一些内容(例如动画),之后或者点击后才能看到另外的内容,或者当点击按钮后才出现的某些图标和动效。可以设置成预制体,需要时再加载。
** 注意权衡加载速度与内存**,过大的UI固然加载缓慢内存消耗大,但拆分成小个体时,如果小个体频繁加载和销毁,也同样会消耗过多CPU。如果加载和销毁过于频繁,我们可以使用后面介绍的优化方法,把它们存起来不销毁。
4.6.3 UI 预加载
为什么要进行 UI 的预加载?
对于一些容易比较小,难以拆分的 UI 界面,就很难再用拆分的方法优化。有的 UI 界面即使拆分后,任然会消耗很多 CPU。对此我们可以使用 UI 预加载,在游戏开始前或者在进入某个场景前预先加载一些 UI ,让实例化和初始化的消耗在游戏前平均分摊在等待的时间线上。
如何进行 UI 预加载?
最直接的方法,在游戏开始前加载UI资源但不实例化,只是把资源加载到内存。
打开界面时 CPU 消耗还是很严重,那么就将 UI 实例化和初始化也提前到游戏开始前。
所有的预加载都会出现另一个问题,即 **CPU 集中消耗会带来卡顿现象**。因此应该尽量地分散这些消耗。
4.6.4 UI图集 Alpha 分离
为什么要对UI图集进行Alpha分离?
压缩图集之后,屏幕上显示效果有时会不尽人意。这是因为**在使用压缩模式 ECT 或 PVRTC 时将透明通道也一并压缩进去了,导致了渲染的扭曲**,因此我们需要把透明通道 alpha 分离出来单独压缩。这样就既可以压缩图集,达到缩小内存目的,图像显示又不会太失真。
如何分离UI图集的Alpha呢?
主要是针对 NGUI 的方案,而 UGUI 由于是内部集成的,Alpha 分离在 Unity3D 的 UGUI 中已经帮你完成了,因此我们仅仅针对 NGUI 来讲一讲如何分离 alpha。
在 NGUI 中,**Alpha 分离的原理是将原本以 ETC的方式压缩一张图改为压缩一张没有 Alpha 的图和一张有 Alpha 的图。**
因为暂未使用 NGUI 所以这里不详细介绍,有需求的可以看看作者原文。《Unity3D高级编程之进阶主程》第四章,UI(七) - UI优化(二) - 技术人生 - 编程技术 - JESSE人生 (luzexi.com)
4.6.5 UI 字体拆分
为什么要拆分UI字体?
项目中字体其实占了很大的空间,如果有几个不同的字体一起展示在屏幕上,会消耗较大的内存。拆分字体会让加载字体的速度更快,让场景加载的速度更快。
如何拆分UI字体?
**把字体中的常用字拆出来**,另外生成一个字体文件,让字体文件变小,内存变少,最终使得加载变快。
还可以**针对不同的语言进行拆分**,可以将各种语言都拆分成独立的字体包,每个语种版本只加载自己的字体。
4.6.6 Scroll View 优化
Scroll View 常用在类似背包的界面中,通常背包界面会有大量的元素存在,它们在窗口中不停地滚动会导致合批和渲染裁剪,在生成和滑动 UI 元素时,会消耗大量的 CPU 来重构网格,进而导致画面缓慢,出现卡顿严重。
要优化这种情况,就必须对滚屏菜单组件进行改造,**将原来策略中所有元素都必须一次性实例化的问题改为只实例化需要显示的实例数量。**通过对不可见的元素进行移动和重新设置属性,重复利用元素。
4.6.7 网格重构的优化
为什么在UGUI的图元素在改变颜色或 Alpha 后会导致网格的重构?
UGUI 系统的**网格合并机制**是:只有将拥有**相同材质球**的网格合并到一起,才能达到最佳效果。一个材质球对应一个图集,只有相同图集内的图片才需要合并在一起。
在 UGUI 系统中,当元素需要对改变颜色时,是通过改变顶点的颜色来实现的,即**改变当前元素的顶点颜色**,然后将它们**重新合并到整块的网格**里去。因为**不能直接从原来合并好的网格上找到当前的顶点位置**,所以需要一次性合并重构网格。
Alpha 本身是 Color 里的一个浮点数,附在顶点上的一个属性,所以改变 Alpha 就是改变顶点的 Color 属性。
在 UI 动画里,每一帧都会改变 UGUI 的颜色和 Alpha,UGUI的每一帧也都会对网格进行一次重构。即使动静分离也无济于事。
如何对此做优化呢?
自己建一个材质球,提前告诉 UGUI:**我们使用自己的特殊的材质球进行渲染**。当颜色动画对颜色和 Alpha 更改时,我们直接**基于自定义的材质球改变颜色和 Alpha **。
这样 UGUI 就不需要重构网格了,因为把渲染的工作交给了新的材质球,而不是通过 UGUI 设置顶点颜色和新材质球来达到效果。
首先,我们需要把UGUI的Shader下载下来。
然后,建立一个自己的材质球,并且材质球里使用下载下来的UGUI的Shader。
再次,把这个材质球放入Image或RawImage的Material上去,与Image或RawImage绑定。
接着,写个类比如class ImageColor继承MonoBehaviour,里面有个public 的颜色变量,比如public Color mColor,类里面只干一件事,在update里一直判断是否需要更改颜色,如果颜色被更改,就把颜色赋值给Material。
最后,把动画文件中的颜色部分从更改Image或RawImage的颜色变为更改 ImageColor 的颜色变量。这样UGUI颜色动画在播放时,不会直接去改变 Image 或 RawImage 的颜色,改变的是我们创建的 ImageColor 的颜色。
通过 ImageColor 来改变材质球属性,最后达到不重构网格的效果。切换元素的贴图时也一样可以做到不重构的效果,在没有改造的情况下,贴图更换会导致重构。为了达到不重构的目的,可以放上自己的材质球并且更换材质球中的贴图。
**注意:**
- 因为启用了自定义的材质球,每个材质球都会单独增加一次 Drawcall,所以导致 Drawcall 增加。
- 并且当 Alpha 不是1的时候,会与原有的 UGUI 产生的半透明材质球形成混淆的渲染排序,原因是当多张透贴放在一起渲染时,Alpha blend 会导致渲染排序混乱、前后不一致的现象。
- 半透明物体的排序问题,归根结底是无法写入深度数据的问题,是模型重叠渲染中无法彻底解决的问题。这可以通过改变自定义的着色器中的渲染次序(RenderQueue)来处理。
4.6.8 UI 展示与关闭的优化
关闭和打开的CPU消耗的优化策略:
利用碎片时间预加载
在关闭时隐藏节点而不是销毁。关闭和激活时,内存没有变化,但网格重构和组件激活会有大量的 CPU 消耗。
移出屏幕代替隐藏更好。移出屏幕后,需要关闭关闭脚本上的更新内容;移入屏幕时,需要进行初始化回到原来的状态。
移出屏幕后,相机仍然会对其进行裁剪判断,因此还需要设置 UI 为不可见的层级 Layout,使其排除在相机渲染之外,需要展示时再设置回 UI 层级。
可以根据不同情况多种方式平行使用。
4.6.9 对象池的运用
**对象池:**即对象的池子。对象池里寄存着一些废弃的对象,当计算机程序需要该种对象时,可以向对象池申请,让我们对废弃的对象再利用,其实质是内存重复利用的性能优化方案。
对废弃的内存再利用就能省去了很多**内存碎片问题和 GC 问题**,还能节省实例化时 **CPU 的消耗**。其中实例化消耗包括模型文件读取、贴图文件读取、GameObject 实例化、程序逻辑初始化、内存分配消耗等。
** 对象池规则:**需要对象时向对象池申请对象,对象池从池子中拿出以前废弃的对象重新“清洗”后给出去,如果对象池也没有可用对象,则新建一个放入给出去,在对象用完后,仍把这些废弃的对象放入对象池以便再利用。
对象池的方法本质是**重复利用内存**。好处:
减少内存碎片,内存连续的可能变大,CPU 缓存命中的概率也变大了。
内存的使用量也会减少。
内存垃圾及触发垃圾回收的次数也少了。
不恰当使用可能引起的问题:如果对象池的回收被错误地重复操作,不但会导致新的逻辑错误,还可能导致内存被撑大。
运用对象池的经验:
- 当程序中有重复实例化并不断销毁的对象时需要使用对象池进行优化。重复实例化和销毁操作会消耗大量 CPU,在此类对象上使用对象池的优化效果极佳。
- 每个需要使用对象池的对象都需要继承对象池的基类对象,这样在初始化时可以针对不同对象做重载,区别对待不同类型的对象,让不同对象的初始化方法根据各自的情况分别初始化。
- 销毁操作时使用对象池接口进行回收,切记不要重复回收对象,也不要错误地放弃回收。在销毁物体时要使用对象池提供的销毁接口,以便让对象池集中存储对象。
- 场景结束时要及时销毁整个对象池,避免无意义的内存驻留。
4.6.10 UI 贴图设置的优化
Unity3D 会重置全部贴图格式。可以理解为,无论你是 JPG,PNG,PSD 等,只要放在 Unity3D 中,Unity3D 会**读取图片内容**,然后**重新生成一个自己格式的图**,也就是说,它在引擎中使用的是自己生成的图和格式。
Unity3D 中图片的设置也有很多讲究,因为关系到重新生成的图片的格式,最终将决定加载入引擎的是什么样格式的图片,所以我们不得不要研究下贴图的设置问题。
NGUI 的 UI 贴图使用**传统的贴图方式**,常使用 **Editor GUI** 和 **Legacy GUI** 方式,这种方式隐藏了一些设置参数,只有全面掌握所有相关功能,才能做好优化工作,可把Editor GUI 和 Legacy GUI 方式展开为 **Advance **类型。
Advance 里面需要注意的问题:
是否需要 Alpha 通道。如果需要 Alpha 通道,则要把 Alpha 通道点开,否则最好关闭。
是否需要进行2次方大小的大小纠正。对UI贴图来说基本上都是2次方大小的图集,使用对象大多是头像之类的 Icon。
去除读写权限。常会默认勾选,导致内存量大增,此选项会使贴图在内存中存储两份,内存会有比不勾选大1倍。
去除 Mipmap。Mipmap 是对 3D 远近视觉的优化,Mipmap 会在摄像头离物体远时因为不需要高清的图片而选择使用 Mipmap 生成的贴图小的模糊图像,从而减轻 GPU 压力。但是在 2D 界面上没有远近之分,所以并不需要 Mipmap 这个选项,而且 Mipmap 会导致内存和磁盘空间加大,会使得 UI 看起来模糊。
选择压缩方式。压缩方式的选择,主要是为了降低内存消耗,降低 CPU 与 GPU 之间的带宽消耗,以及减少包体的大小,在清晰度足够的情况下,我们可以针对性的选择一些压缩方式来优化内存和包体。
**无压缩 > RGBA16 > RGB24 > RGB16 > RGBA ECT2 8位 > RGBA PVRTC 4位 > RGB ECT2 4位 > RGB PVRTC 4位。** 这样逐级下来,压缩的越来越厉害,同时画质就越来越差。前面有介绍过关于 UI 贴图 Alpha 分离的方法,这方法就是压缩的极致和平衡,既做到好画质又最大极限的压缩了图片。 我们可以通过**写脚本的方式**,把放入 UI 的贴图自动设置我们规定好的图片选项,辅助我们更改 UI 贴图设置,省去不少二次检查时间。例如以下这段,就是利用 Unity3D 的 Editor API 来自动设置 UGUI 的精灵图片。
void Apply_ui_sprite()
{
if(!UIAssetPost.IsInPath(assetImporter.assetPath, UI_Sprite_path))
{
return;
}
TextureImporter tex_importer = assetImporter as TextureImporter;
if(tex_importer == null) return;
tex_importer.textureType = TextureImporterType.Sprite;
FileInfo file_info = new FileInfo(assetImporter.assetPath);
string dir_name = file_info.Directory.Name;
tex_importer.spritePackingTag = dir_name;
tex_importer.alphaIsTransparency = true;
tex_importer.mipmapEnabled = false;
tex_importer.wrapMode = TextureWrapMode.Clamp;
tex_importer.isReadable = false;
SetCompress(tex_importer);
}
版权归原作者 仁希' 所有, 如有侵权,请联系我们删除。