Unity官方博文翻译——UGUI优化05
首先附上原文地址:https://unity3d.com/cn/learn/tutorials/topics/best-practices/optimizing-ui-controls
优化UI控件
UGUI优化指南的这一部分关注于一些特殊类型UI的问题。虽然说大多数UI控件在性能方面都比较类似,但有两个组件会使游戏在接近发布状态时造成许多性能问题。
UI Text
Unity内置的Text组件是在UI内显示栅格化字形的一种很便捷的方式。然而有一些并不被广泛了解的特性,作为性能热点高频率的出现。每当给UI增加text的时候,要知道字形事实上是作为独立的四边形渲染的,每个字符一个。这些四边形周围通常有大量的多余闲置空间,这取决于它们的形状,text这种位置的特点造成了一不小心就会打断其他UI元素的batch。
Text的网格rebuild
一个主要的问题是重建UI text网格,每当text组件被改变时,文本组件都必须重新计算多边形来显示实际的text。这个重新计算也发生在这个text组件或是它的父游戏物体被禁用或者启用时,虽然这个text没有改变。
这个行为对于任何要显示大量文本标签的UI都是一个问题,比如说大部分的排行榜或是统计界面。由于隐藏和显示UGUI最简单的方式是启用和禁用包含UI的游戏物体,当拥有大量text组件的UI显示时将经常会出现不期望的帧率卡顿。
有关这个问题的潜在解决方案,请看下一章的Disabling Canvas Renderers部分。(链接见原文)
动态字体和字体图集
当要显示的字符集合非常大或者在运行前并不知道有多少时,动态字体是一个非常方便的方式来显示文本。Unity实现时,这些字体基于UI Text组件中遇到的字符,在运行是构建一个字符图集。
加载每个字体对象将会保持自己纹理图集,即使它与另一个字体在相同的字体体系中。例如,使用Arial字体的加粗的文本在一个控件上,同时使用Arial Bold字体在另外一个控件上将会产生一样的显示输出。但是Unity将会保存两份独立的纹理图集,一个是Arial字体另一个是Arial Bold字体。
从一个优化的角度来讲,要理解的最重要的事情是UGUI的动态字体图集保存了每个单独的由尺寸、风格、字符排列组合而成的字形。也就是说,如果一个UI包含两个text组件,都显示字母“A”,则:
1.如果两个Text组件拥有相同的大小,则字体图集中将包含一个字形。
2.如果两个text组件不拥有相同的大小(例如,一个是16号,另一个是24号),则字体图集将包含两个不同大小的字母“A”的副本。
3.如果一个text组件是粗体而另一个不是,则字体图集将同时包含粗体的“A”和常规的“A”。
无论何时一个拥有动态字体的UI text对象遇到了一个没有被栅格化到字体纹理图集的字形时,字体纹理图集就必须被重建。如果新的字形可以适配进当前的图集,它将被添加进去,并且图形设备会重新加载这个图集。然而,如果目前的图集太小了,系统将会尝试重建图集,分为两阶段。
首先,图集会以相同的大小重建,只会用到当前激活的UI text组件(包括其父级Canvas启用,但是禁用了Canvas Renderer组件的的UI Text组件)中要显示的字形,如果系统成功的将所有现在要用到的字形适配进新图集中,图集的栅格化不会继续进入第二步。
其次,如果这些现在使用到的字形不行被适配进当前大小的图集,一个更大的图集将会被创建出来,将这个图集较短的尺寸放大二倍。比如,512x512的图集将会被扩大到512x1024。
由于上述算法,一个动态字体图集只会在被创建后增长尺寸。考虑到重建纹理图集的性能消耗,使重建最小化是非常必要的。有两种方式。
如果有可能,使用非静态字体,并预先配置好对所需字形的支持。这通常适用于对字符有非常好限制的UI,比如说只有拉丁文/ASCII字符,或者其他一个较小的范围。
如果必须支持一个非常大范围的字符,比如全部的万国码,字体必须被设为动态字体。为避免可预见的性能问题,主要的字体字形图集在一开始就要通过Font.RequestCharactersInTexture设置适当的字符。
请记住,字体图集的重建是是在每个UI Text组件被改变时才会被单独触发。当生成极多text组件时,在text组件内容和主要的字体图集中收集所有独特的字符是有好处的。这将会确认字形图集只需要被重建一次而不是每次新字形生成时都会要重建。
另外请注意,当一个字体图集的重建被触发时,任何目前不包含在激活的UI Text组件中的字符将不会被添加到新的图集中,虽然他们一开始调用Font.RequestCharactersInTexture被添加到图集中。要解决此限制,请订阅Font.textureRebuilt这个委托并且查询Font.characterInfo来确保所有期望的字符保持在图集中。
Font.textureRebuilt委托目前没有公开,它是一个单参数的Unity事件,参数是纹理图集要被重建的字体,此事件的订阅者应当遵循以下签名:public void TextureRebuiltCallback(Font rebuiltFont) { /* ... */ }
专门的字形渲染器
由于字形众所周知的情况,在每个字形位置相对固定的情况下,为这些要显示的sprite写一个用户自定义的组件来显示这些字形明显是更有益的,比分显示就是一个例子。
对于比分,要显示的字符就是从众所周知的数字0-9中绘制,不会改变范围,并且以彼此固定的距离显示。将一个整数分解为数码并且显示合适的数码sprite是相对来说无关紧要的。这种专门的数字显示系统以一种无需配置和非常快的计算、动画和显示速度来构建,比Canvas的UI Text组件强很多。
后备字体和内存使用
对于必须支持大字符集的应用程序,很容易在字体导入器的“Font Names”字段中列出大量的字体。列在 “Font Names” 字段内的任何字体将会作为后备字体,在字形不能在主要字体中找到的时候。后备字体的排列顺序决定于它们列在“Font Names” 字段内的顺序。
然而,为了支持这种行为,Unity会将“Font Names”字段中列出的所有字体加载到内存中。如果一个字体的字符集非常大,则后备字体的内存消耗将过多。这种情况经常发生在包括象形文字的字体,比如日本汉字或者是中文字符。
Best Fit和性能
一般来说,永远不要使用UI text组件的Best Fit设置。
Best Fit动态的适配一个字体的最大整数大小,使text组件能够显示在框体中不会超出框体,并限定一个可配置的最大/最小的字体尺寸。然而,由于Unity会把每个要显示的独立的字形的每个单独的尺寸渲染进字体图集,所以使用Best Fit的话字体图集将会被迅速的被不同的尺寸的字形所填满。
从Unity5.3开始,Best Fit使用的尺寸检测是非最佳的。它将每一个测试的增加尺寸的字形生成到字体图集中,这远远增加了生成字体图集所要花费的时间。这也倾向于导致图集溢出,旧字形被踢出图集。由于Best Fit计算所需大量测试,这会经常将别的text组件使用的字形驱逐,在合适的字体大小被计算出来以后,字体图集的重建将会被强制执行至少一次。这个特别的问题在Unity5.4中被改正,并且Best Fit不会不必要的增加字体纹理图集,但它仍旧比静态大小的text要慢的多。
频繁的字体图集重建会迅速降低运行时性能,并导致内存碎片。设置为Best Fit的text组件的数量越多,这个问题就越严重。
Scroll View
在填充率问题之后,UGUI的Scroll View是第二个运行时的最常见性能问题的源头。Scroll View通常需要大量的UI元素来表现其内容。Scroll View的填充有两个基本的方法:
1.用Scroll View内容的所有要显示的必要的元素来填充。
2.将元素存储起来,当它们需要表现可视内容的时候重新给他们设定合适的位置。
这两种解决方案都有问题。
第一个解决方案需要更多的时间来生成所有UI元素,而且随着要显示的元素的增加,Scroll View的rebuilt的所需时间也会跟着增加。如果Scroll View中只有少量的元素,比如Scroll View只需显示一些Text组件,那么使用这个解决方案简单易行,最为合适。
第二个解决方案需要大量的代码来实现正确的当前要显示的UI和layout系统。下面将进一步详细讨论两种可行的解决方案。对于那些大量的复杂的要滚动的UI来说,通常需要使用某些对象池的方法来避免性能问题。
尽管存在这些问题,所有的方法都能够通过给Scroll View添加RectMask2D组件来得到改进。这个组件可以确保Scroll View的viewport之外的元素不包括在要绘制的元素列表中,在列表中的元素在Canvas的rebuild的时候必须生成它们的几何体,排列,并且进行分析。
简单的Scroll View元素池
实现Scroll View对象池最简单同时保留Unity内置的Scroll View组件的大多数原生便捷性的方法是使用一种混合的方法:
在UI中布局元素,这将允许布局系统来正确计算Scroll View的content大小,并且允许scrollbar正常工作,使用挂载Layout Element组件的游戏物体来作为可见UI元素的“placeholders(占位符)”。
然后,实例化足以填充Scroll View的可视区域的可视部分的可见UI元素,并且设定父物体为定位好的占位符。当Scroll View滚动时,重用UI元素来显示滚动到视口中的内容。
这将大大减少必须要batch的UI元素的数量,因为batch的开销增长只基于Canvas内的Canvas Renderer数量,而不是Rect Transform的数量。
解决问题的简单方法
目前,当任何UI元素重新设置父级,或者层级排序变换的时候,该元素及其所有的子元素都将被标“Dirty”,并且会强制rebuild其Canvas。
这个问题的原因是Unity没有分离重新设置父级和改变其层级排序的回调。这些事件都会触发一个叫OnTransformParentChanged的回调。在UGUI源码的Graphic类中(见源码中的Graphic.cs脚本),这个回调实现并执行了SetAllDirty方法。通过将Graphic标Dirty,系统可以确保Graphic将在下一帧渲染前重建其布局和顶点。
可以将Canvas分配到Scroll View中每一个元素的根RectTransform上,这将会限制重建发生的范围只在重新设置父级的元素上而不是Scroll View的整个content。但是这也会增加需要渲染Scroll View的drawcall数量。此外,如果Scroll View内独立的元素是复杂的并且包含了十多个Graphic组件,特别是每个元素上包含了大量的Layout组件,其rebuild的开销也经常可以会高到显著的降低低端设备的帧率。
如果一个Scroll View的UI元素的大小是不可变的,那么完全重新计算布局和顶点就是不必要的。然而,避免这种问题的解决方案需要实现的对象池是基于位置的改变而不是重新设置父级或是更改层级顺序。
基于位置改变的Scroll View池
为了避免上述问题,可以创建一个仅仅是移动其包含的UI元素的RectTransform的对象存储池。这样就通过移动RectTransform避免了rebuild尺寸未改变的内容,显著的提高了Scroll View的性能。
要做到这一点,通常最好是要写一个用户自定义的Scroll View子类和写一个用户自定义的Layout Group组件。后者通常是更简单的解决方案,可以通过实现UGUI的LayoutGroup的抽象基类来完成。
用户自定义的LayoutGroup可以分析底层的元数据来检查要显示多少数据元素,并且可以重新设置Scroll View的content的RectTransform为合适的。也可以通过订阅Scroll View change events(onValueChanged)并相应的使用来重新设置可见元素的位置。