首先附上原文地址:https://unity3d.com/cn/learn/tutorials/topics/best-practices/optimizing-ui-controls
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?優(yōu)化UI控件
? ? ? ? UGUI優(yōu)化指南的這一部分關(guān)注于一些特殊類型UI的問題驾窟。雖然說大多數(shù)UI控件在性能方面都比較類似物喷,但有兩個組件會使游戲在接近發(fā)布狀態(tài)時造成許多性能問題。
UI Text
? ? ? ? Unity內(nèi)置的Text組件是在UI內(nèi)顯示柵格化字形的一種很便捷的方式弯洗。然而有一些并不被廣泛了解的特性,作為性能熱點高頻率的出現(xiàn)宏榕。每當給UI增加text的時候独泞,要知道字形事實上是作為獨立的四邊形渲染的,每個字符一個悼沈。這些四邊形周圍通常有大量的多余閑置空間,這取決于它們的形狀姐扮,text這種位置的特點造成了一不小心就會打斷其他UI元素的batch絮供。
Text的網(wǎng)格rebuild
? ? ? ? ?一個主要的問題是重建UI text網(wǎng)格,每當text組件被改變時茶敏,文本組件都必須重新計算多邊形來顯示實際的text壤靶。這個重新計算也發(fā)生在這個text組件或是它的父游戲物體被禁用或者啟用時,雖然這個text沒有改變惊搏。
? ? ? ? 這個行為對于任何要顯示大量文本標簽的UI都是一個問題贮乳,比如說大部分的排行榜或是統(tǒng)計界面忧换。由于隱藏和顯示UGUI最簡單的方式是啟用和禁用包含UI的游戲物體,當擁有大量text組件的UI顯示時將經(jīng)常會出現(xiàn)不期望的幀率卡頓向拆。
? ? ? ? 有關(guān)這個問題的潛在解決方案亚茬,請看下一章的Disabling Canvas Renderers部分。(鏈接見原文)
動態(tài)字體和字體圖集
? ? ? ? 當要顯示的字符集合非常大或者在運行前并不知道有多少時浓恳,動態(tài)字體是一個非常方便的方式來顯示文本才写。Unity實現(xiàn)時,這些字體基于UI Text組件中遇到的字符奖蔓,在運行是構(gòu)建一個字符圖集。
? ? ? ? 加載每個字體對象將會保持自己紋理圖集讹堤,即使它與另一個字體在相同的字體體系中吆鹤。例如,使用Arial字體的加粗的文本在一個控件上洲守,同時使用Arial Bold字體在另外一個控件上將會產(chǎn)生一樣的顯示輸出疑务。但是Unity將會保存兩份獨立的紋理圖集,一個是Arial字體另一個是Arial Bold字體梗醇。
? ? ? ? ?從一個優(yōu)化的角度來講知允,要理解的最重要的事情是UGUI的動態(tài)字體圖集保存了每個單獨的由尺寸、風格叙谨、字符排列組合而成的字形温鸽。也就是說,如果一個UI包含兩個text組件手负,都顯示字母“A”涤垫,則:
? ? ? ? 1.如果兩個Text組件擁有相同的大小,則字體圖集中將包含一個字形竟终。
? ? ? ? ?2.如果兩個text組件不擁有相同的大序疴(例如,一個是16號统捶,另一個是24號)榆芦,則字體圖集將包含兩個不同大小的字母“A”的副本。
? ? ? ? ?3.如果一個text組件是粗體而另一個不是喘鸟,則字體圖集將同時包含粗體的“A”和常規(guī)的“A”匆绣。
? ? ? ? 無論何時一個擁有動態(tài)字體的UI text對象遇到了一個沒有被柵格化到字體紋理圖集的字形時,字體紋理圖集就必須被重建什黑。如果新的字形可以適配進當前的圖集犬绒,它將被添加進去,并且圖形設(shè)備會重新加載這個圖集兑凿。然而凯力,如果目前的圖集太小了茵瘾,系統(tǒng)將會嘗試重建圖集,分為兩階段咐鹤。
? ? ? ? ?首先拗秘,圖集會以相同的大小重建,只會用到當前激活的UI text組件(包括其父級Canvas啟用祈惶,但是禁用了Canvas Renderer組件的的UI Text組件)中要顯示的字形雕旨,如果系統(tǒng)成功的將所有現(xiàn)在要用到的字形適配進新圖集中,圖集的柵格化不會繼續(xù)進入第二步捧请。
? ? ? ? 其次凡涩,如果這些現(xiàn)在使用到的字形不行被適配進當前大小的圖集,一個更大的圖集將會被創(chuàng)建出來疹蛉,將這個圖集較短的尺寸放大二倍活箕。比如,512x512的圖集將會被擴大到512x1024可款。
? ? ? ? 由于上述算法育韩,一個動態(tài)字體圖集只會在被創(chuàng)建后增長尺寸」刖ǎ考慮到重建紋理圖集的性能消耗字逗,使重建最小化是非常必要的肥橙。有兩種方式晒他。
? ? ? ? 如果有可能璧南,使用非靜態(tài)字體,并預先配置好對所需字形的支持立镶。這通常適用于對字符有非常好限制的UI蛮粮,比如說只有拉丁文/ASCII字符,或者其他一個較小的范圍谜慌。
? ? ? ? ?如果必須支持一個非常大范圍的字符然想,比如全部的萬國碼,字體必須被設(shè)為動態(tài)字體欣范。為避免可預見的性能問題变泄,主要的字體字形圖集在一開始就要通過Font.RequestCharactersInTexture設(shè)置適當?shù)淖址?/p>
? ? ? ? 請記住,字體圖集的重建是是在每個UI Text組件被改變時才會被單獨觸發(fā)恼琼。當生成極多text組件時妨蛹,在text組件內(nèi)容和主要的字體圖集中收集所有獨特的字符是有好處的。這將會確認字形圖集只需要被重建一次而不是每次新字形生成時都會要重建晴竞。
? ? ? ? 另外請注意蛙卤,當一個字體圖集的重建被觸發(fā)時,任何目前不包含在激活的UI Text組件中的字符將不會被添加到新的圖集中,雖然他們一開始調(diào)用Font.RequestCharactersInTexture被添加到圖集中颤难。要解決此限制神年,請訂閱Font.textureRebuilt這個委托并且查詢Font.characterInfo來確保所有期望的字符保持在圖集中。
? ? ? ? Font.textureRebuilt委托目前沒有公開行嗤,它是一個單參數(shù)的Unity事件已日,參數(shù)是紋理圖集要被重建的字體,此事件的訂閱者應當遵循以下簽名:public void TextureRebuiltCallback(Font rebuiltFont) { /* ... */ }
專門的字形渲染器
由于字形眾所周知的情況栅屏,在每個字形位置相對固定的情況下飘千,為這些要顯示的sprite寫一個用戶自定義的組件來顯示這些字形明顯是更有益的,比分顯示就是一個例子栈雳。
對于比分护奈,要顯示的字符就是從眾所周知的數(shù)字0-9中繪制,不會改變范圍哥纫,并且以彼此固定的距離顯示霉旗。將一個整數(shù)分解為數(shù)碼并且顯示合適的數(shù)碼sprite是相對來說無關(guān)緊要的。這種專門的數(shù)字顯示系統(tǒng)以一種無需配置和非郴腔快的計算、動畫和顯示速度來構(gòu)建抛虫,比Canvas的UI Text組件強很多松靡。
后備字體和內(nèi)存使用
對于必須支持大字符集的應用程序,很容易在字體導入器的“Font Names”字段中列出大量的字體建椰。列在 “Font Names” 字段內(nèi)的任何字體將會作為后備字體雕欺,在字形不能在主要字體中找到的時候。后備字體的排列順序決定于它們列在“Font Names” 字段內(nèi)的順序棉姐。
然而屠列,為了支持這種行為,Unity會將“Font Names”字段中列出的所有字體加載到內(nèi)存中伞矩。如果一個字體的字符集非常大笛洛,則后備字體的內(nèi)存消耗將過多。這種情況經(jīng)常發(fā)生在包括象形文字的字體乃坤,比如日本漢字或者是中文字符苛让。
Best Fit和性能
一般來說,永遠不要使用UI text組件的Best Fit設(shè)置湿诊。
Best Fit動態(tài)的適配一個字體的最大整數(shù)大小狱杰,使text組件能夠顯示在框體中不會超出框體,并限定一個可配置的最大/最小的字體尺寸厅须。然而仿畸,由于Unity會把每個要顯示的獨立的字形的每個單獨的尺寸渲染進字體圖集,所以使用Best Fit的話字體圖集將會被迅速的被不同的尺寸的字形所填滿。
從Unity5.3開始错沽,Best Fit使用的尺寸檢測是非最佳的簿晓。它將每一個測試的增加尺寸的字形生成到字體圖集中,這遠遠增加了生成字體圖集所要花費的時間甥捺。這也傾向于導致圖集溢出抢蚀,舊字形被踢出圖集。由于Best Fit計算所需大量測試镰禾,這會經(jīng)常將別的text組件使用的字形驅(qū)逐皿曲,在合適的字體大小被計算出來以后,字體圖集的重建將會被強制執(zhí)行至少一次吴侦。這個特別的問題在Unity5.4中被改正屋休,并且Best Fit不會不必要的增加字體紋理圖集,但它仍舊比靜態(tài)大小的text要慢的多备韧。
頻繁的字體圖集重建會迅速降低運行時性能劫樟,并導致內(nèi)存碎片。設(shè)置為Best Fit的text組件的數(shù)量越多织堂,這個問題就越嚴重叠艳。
Scroll View
在填充率問題之后,UGUI的Scroll View是第二個運行時的最常見性能問題的源頭易阳。Scroll View通常需要大量的UI元素來表現(xiàn)其內(nèi)容附较。Scroll View的填充有兩個基本的方法:
1.用Scroll View內(nèi)容的所有要顯示的必要的元素來填充。
2.將元素存儲起來潦俺,當它們需要表現(xiàn)可視內(nèi)容的時候重新給他們設(shè)定合適的位置拒课。
這兩種解決方案都有問題。
第一個解決方案需要更多的時間來生成所有UI元素事示,而且隨著要顯示的元素的增加早像,Scroll View的rebuilt的所需時間也會跟著增加。如果Scroll View中只有少量的元素肖爵,比如Scroll View只需顯示一些Text組件卢鹦,那么使用這個解決方案簡單易行,最為合適劝堪。
第二個解決方案需要大量的代碼來實現(xiàn)正確的當前要顯示的UI和layout系統(tǒng)法挨。下面將進一步詳細討論兩種可行的解決方案。對于那些大量的復雜的要滾動的UI來說幅聘,通常需要使用某些對象池的方法來避免性能問題凡纳。
盡管存在這些問題,所有的方法都能夠通過給Scroll View添加RectMask2D組件來得到改進帝蒿。這個組件可以確保Scroll View的viewport之外的元素不包括在要繪制的元素列表中荐糜,在列表中的元素在Canvas的rebuild的時候必須生成它們的幾何體,排列,并且進行分析暴氏。
簡單的Scroll View元素池
實現(xiàn)Scroll View對象池最簡單同時保留Unity內(nèi)置的Scroll View組件的大多數(shù)原生便捷性的方法是使用一種混合的方法:
在UI中布局元素延塑,這將允許布局系統(tǒng)來正確計算Scroll View的content大小,并且允許scrollbar正常工作答渔,使用掛載Layout Element組件的游戲物體來作為可見UI元素的“placeholders(占位符)”关带。
然后,實例化足以填充Scroll View的可視區(qū)域的可視部分的可見UI元素沼撕,并且設(shè)定父物體為定位好的占位符宋雏。當Scroll View滾動時,重用UI元素來顯示滾動到視口中的內(nèi)容务豺。
這將大大減少必須要batch的UI元素的數(shù)量磨总,因為batch的開銷增長只基于Canvas內(nèi)的Canvas Renderer數(shù)量,而不是Rect Transform的數(shù)量笼沥。
解決問題的簡單方法
目前蚪燕,當任何UI元素重新設(shè)置父級,或者層級排序變換的時候奔浅,該元素及其所有的子元素都將被標“Dirty”,并且會強制rebuild其Canvas馆纳。
這個問題的原因是Unity沒有分離重新設(shè)置父級和改變其層級排序的回調(diào)。這些事件都會觸發(fā)一個叫OnTransformParentChanged的回調(diào)汹桦。在UGUI源碼的Graphic類中(見源碼中的Graphic.cs腳本)鲁驶,這個回調(diào)實現(xiàn)并執(zhí)行了SetAllDirty方法。通過將Graphic標Dirty,系統(tǒng)可以確保Graphic將在下一幀渲染前重建其布局和頂點营勤。
可以將Canvas分配到Scroll View中每一個元素的根RectTransform上灵嫌,這將會限制重建發(fā)生的范圍只在重新設(shè)置父級的元素上而不是Scroll View的整個content壹罚。但是這也會增加需要渲染Scroll View的drawcall數(shù)量葛作。此外,如果Scroll View內(nèi)獨立的元素是復雜的并且包含了十多個Graphic組件猖凛,特別是每個元素上包含了大量的Layout組件赂蠢,其rebuild的開銷也經(jīng)常可以會高到顯著的降低低端設(shè)備的幀率辨泳。
如果一個Scroll View的UI元素的大小是不可變的虱岂,那么完全重新計算布局和頂點就是不必要的。然而菠红,避免這種問題的解決方案需要實現(xiàn)的對象池是基于位置的改變而不是重新設(shè)置父級或是更改層級順序第岖。
基于位置改變的Scroll View池
為了避免上述問題,可以創(chuàng)建一個僅僅是移動其包含的UI元素的RectTransform的對象存儲池试溯。這樣就通過移動RectTransform避免了rebuild尺寸未改變的內(nèi)容蔑滓,顯著的提高了Scroll View的性能。
要做到這一點,通常最好是要寫一個用戶自定義的Scroll View子類和寫一個用戶自定義的Layout Group組件键袱。后者通常是更簡單的解決方案燎窘,可以通過實現(xiàn)UGUI的LayoutGroup的抽象基類來完成。
用戶自定義的LayoutGroup可以分析底層的元數(shù)據(jù)來檢查要顯示多少數(shù)據(jù)元素蹄咖,并且可以重新設(shè)置Scroll View的content的RectTransform為合適的褐健。也可以通過訂閱Scroll View change events(onValueChanged)并相應的使用來重新設(shè)置可見元素的位置。