關於Unity中的UGUI優化,你可能遇到這些問題

weixin_33763244發表於2018-02-05

一、介面製作

Q1:UGUI裡的這個選項 ,應該是ETC2拆分Alpha通道的意思,但是在使用中並沒起作用?請問有沒有什麼拆分的標準和特別要求呢?
請輸入圖片描述

據我們所知,alpha split 的功能最初只對 Unity 2D 的 Sprite(SpriteRenderer)有完整的支援,而UI的支援是在Unity 5.4版本之後的。建議大家在Unity 5.4版本以後的UGUI中嘗試該功能。

Q2:在UI介面中,用Canvas還是用RectTransform做根節點更好?哪種方法效率更高?

Canvas劃分是個很大的話題。簡單來說,因為一個Canvas下的所有UI元素都是合在一個Mesh中的,過大的Mesh在更新時開銷很大,所以一般建議每個較複雜的UI介面,都自成一個Canvas(可以是子Canvas),在UI介面很複雜時,甚至要劃分更多的子Canvas。同時還要注意動態元素和靜態元素的分離,因為動態元素會導致Canvas的mesh的更新。最後,Canvas又不能細分的太多,因為會導致Draw Call的上升。我們後續將對UI模組做具體的講解,盡請期待。

Q3:UWA效能檢測報告中的Shared UI Mesh表示什麼呢?

請輸入圖片描述

Shared UI Mesh是在Unity 5.2 版本後UGUI系統維護的UI Mesh。在以前的版本中,UGUI會為每一個Canvas維護一個Mesh(名為BatchedMesh,其中再按材質分為不同的SubMesh)。而在Unity 5.2版本後,UGUI底層引入了多執行緒機制,而其Mesh的維護也發生了改變,目前Shared UI Mesh作為靜態全域性變數,由底層直接維護,其大小與當前場景中所有啟用的UI元素所生成的網格數相關。
一般來說當介面上UI元素較多,或者文字較多時該值都會較高,在使用UI/Effect/shadow和UI/Effect/Outline時需要注意該值,因為這兩個Effect會明顯增加文字所帶來的網格數。

Q4:在使用NGUI時,我們通常會將很多小圖打成一個大的圖集,以優化記憶體和Draw Call。而在UGUI時代,UI所使用的Image必須是Sprite;Unity提供了SpritePacker。 它的工作流程和UGUI Atlas Paker有較大的差別。在Unity Asset中,我們壓根看不到圖集的存在。 問題是:
1. SpritePacker大概的工作機制是什麼樣的?

2. 如果Sprite沒有打包成AssetBundle,直接在GameObject上引用,那麼在Build時Unity會將分散的Sprite拼接在一起麼?如果沒有拼接,那SpritePacker是不是隻會優化Draw Call,記憶體佔用上和不用SpritePacker的分離圖效果一樣?

3. 如果將Sprite打成AssetBundle,AssetBundle中的資源是分散的Sprite嗎?如果不是,不同的AssetBundle中引用了兩張Sprite,這兩張Sprite恰好用SpritePacker拼在了一起,是不是就會存在兩份拼接的Sprite集?

4. 如果想使用NGUI Atlas Packer的工作流程,該如何去實現?

簡單來說,UGUI和 NGUI 類似,但是更加自動化。只需要通過設定 Packing Tag 即可指定哪些 Sprite 放在同一個 Atlas 下。

  1. 可以通過 Edit -> Project Settings -> Editor -> Sprite Packer 的 Mode 來設定是否起效,何時起效(一種是進入 Play Mode 就生效,一種是 Build 時才生效)。所以只要不選 Disabled,Build 時就會把分散的 Sprite 拼起來。
  2. 可以認為 Sprite 就是一個殼子,實際上本身不包含紋理資源,所以打包的時候會把Atlas 打進去。如果不用依賴打包,那麼分開打兩個 Sprite 就意味各自的AssetBundle 裡都會有一個 Atlas。
  3. 可以通過第三方工具(如 Texture Packer)製作 Atlas,匯出 Sprite 資訊(如,第 N 個 Sprite 的 Offset 和 Width,Height 等),然後在 Unity 中通過指令碼將該 Atlas 轉成一個 Multiple Mode 的 Sprite 紋理(即一張紋理上包含了多個 Sprite),同時禁用 Unity 的 Sprite Packer 即可。

兩種做法各有利弊,建議分析一下兩種做法對於自身專案的合適程度來進行選擇。

Q5:在Unity 5.x版本下,我們在用UGUI的過程中發現它把圖集都打進了包裡,這樣就不能自動更新了,請問圖集怎麼做自動更新呢?

在Unity 5.x中UGUI使用的Atlas確實是不可見的,因此無法直接將其獨立打包。但我們建議,可以把Packing Tag相同的源紋理檔案,打到同一個AssetBundle中(設定一樣的AssetBundle Name),從而避免Atlas的冗餘。同時這樣打包可以讓依賴它的Canvas的打包更加自由,即不需要把依賴它的Canvas都打在一個AssetBundle中,在更新時直接更新Atlas所在的AssetBundle即可。

Q6:ScrollRect在滾動的時候,會產生Canvas.SendwillRenderCanvases,有辦法消除嗎?

ScrollRect在滾動時,會產生OnTransformChanged的開銷,這是UI元素在移動時觸發的,但通常這不會觸發Canvas.SendWillRenderCanvases。

如果觀察到Canvas.SendWillRenderCanvases耗時較高,可以檢查下ScrollRect所在的Canvas是否開啟了Pixel Perfect的選項,該選項的開啟會導致UI元素在發生位移時,其長寬會被進行微調(為了對其畫素),而ScrollRect中通常有較多的UI元素,從而產生較高的Canvas.SendWillRenderCanvases開銷。因此可以嘗試關閉Pixel Perfect看效果是否可以接受,或者嘗試在滾動過程中暫時關閉Pixel Perfect等方式來消除其開銷。


二、網格重建

Q1:我在UGUI裡更改了Image的Color屬性,那麼Canvas是否會重建?我只想借用它的Color做Animation裡的變化量。

如果修改的是Image元件上的Color屬性,其原理是修改頂點色,因此是會引起網格的Rebuild的(即Canvas.BuildBatch操作,同時也會有Canvas.SendWillRenderCanvases的開銷)。而通過修改頂點色來實現UI元素變色的好處在於,修改頂點色可以保證其材質不變,因此不會產生額外的Draw Call。

Q2:Unity自帶的UI Shader處理顏色時,改_Color屬性不會觸發頂點重建嗎?

在UI的預設Shader中存在一個Tint Color的變數,正常情況下,該值為常數(1,1,1),且並不會被修改。如果是用指令碼訪問Image的Material,並修改其上的Tint Color屬性時,對UI元素產生的網格資訊並沒有影響,因此就不會引起網格的Rebuild。但這樣做因為修改了材質,所以會增加一個Draw Call。

Q3:能否就UGUI Batch提出一些建議呢?是否有一些Batch的規則?

在 UGUI 中,Batch是以Canvas為單位的,即在同一個Canvas下的UI元素最終都會被Batch到同一個Mesh中。而在Batch前,UGUI會根據這些UI元素的材質(通常就是Atlas)以及渲染順序進行重排,在不改變渲染結果的前提下,儘可能將相同材質的UI元素合併在同一個SubMesh中,從而把DrawCall降到最低。而Batch的操作只會在UI元素髮生變化時才進行,且合成的Mesh越大,操作的耗時也就越大。

因此,我們建議儘可能把頻繁變化(位置,顏色,長寬等)的UI元素從複雜的Canvas中分離出來,從而避免複雜的Canvas頻繁重建。

Q4:我用的是UGUI Canvas,Unity 5.3.4版本,請問如何檢視每次Rebuild Batch影響的頂點數, Memory Profiler是個辦法但是不好定位。

由於Unity引擎在5.2後開始使用Shared UI Mesh來儲存UI Mesh,所以確實很難檢視每次Rebuild的UI頂點數。但是,研發團隊可以嘗試通過Frame Debugger工具對UI介面進行進一步的檢視。

Q5:動靜分離或者多Canvas帶來效能提升的理論基礎是什麼呢?如果靜態部分不變動,整個Canvas就不重新整理了?

在UGUI中,網格的更新或重建(為了儘可能合併UI部分的DrawCall)是以Canvas為單位的,且只在其中的UI元素髮生變動(位置、顏色等)時才會進行。因此,將動態UI元素與靜態UI元素分離後,可以將動態UI元素的變化所引起的網格更新或重建所涉及到的範圍變小,從而降低一定的開銷。而靜態UI元素所在的Canvas則不會出現網格更新和重建的開銷。

Q6:UWA建議“儘可能將靜態UI元素和頻繁變化的動態UI元素分開,存放於不同的Panel下。同時,對於不同頻率的動態元素也建議存放於不同的Panel中。”那麼請問,如果把特效放在Panel裡面,需要把特效拆到動態的裡面嗎?

通常特效是指粒子系統,而粒子系統的渲染和UI是獨立的,僅能通過Render Order來改變兩者的渲染順序,而粒子系統的變化並不會引起UI部分的重建,因此特效的放置並沒有特殊的要求。

Q7:多人同屏的時候,人物移動會使得頭頂上的名字Mesh重組,從而導致較為嚴重的卡頓,請問一下是否有優化的辦法?

如果是用UGUI開發的,當頭頂文字數量較多時,確實很容易引起效能問題,可以考慮從以下幾點入手進行優化:

  1. 儘可能避免使用UI/Effect,特別是Outline,會使得文字的Mesh增加4倍,導致UI重建開銷明顯增大;

  2. 拆分Canvas,將螢幕中所有的頭頂文字進行分組,放在不同的Canvas下,一方面可以降低更新的頻率(如果分組中沒有文字移動,該組就不會重建),另一方面可以減小重建時涉及到的Mesh大小(重建是以Canvas為單位進行的);

  3. 降低移動中的文字的更新頻率,可以考慮在文字移動的距離超過一個閾值時才真正進行位移,從而可以從概率上降低Canvas更新的頻率。


三、介面切換

Q1:遊戲中出現UI介面重疊,該怎麼處理較好?比如當前有一個全屏顯示的UI介面,點其中一個按鈕會再起一個全屏介面,並把第一個UI介面蓋住。我現在的做法是把被覆蓋的介面 SetActive(False),但發現後續 SetActive(True) 的時候會有 GC.Alloc 產生。這種情況下,希望既降低 Batches 又降低 GC Alloc 的話,有什麼推薦的方案嗎?

可以嘗試通過新增一個 Layer 如 OutUI, 且在 Camera 的 Culling Mask 中將其取消勾選(即不渲染該 Layer)。從而在 UI 介面切換時,直接通過修改 Canvas 的 Layer 來實現“隱藏”。但需要注意事件的遮蔽,禁用動態的 UI 元素等等。
這種做法的優點在於切換時基本沒有開銷,也不會產生多餘的 Draw Call,但缺點在於“隱藏時”依然還會有一定的持續開銷(通常不太大),而其對應的 Mesh 也會始終存在於記憶體中(通常也不太大)。
以上的方式可供參考,而效能影響依舊是需要視具體情況而定。

Q2:如圖,我們在UI開啟或者移動到某處的時候經常會觀測到CPU上的衝激,經過進一步觀察發現是因為Instantiate產生了大量的GC。想請問下Instantiate是否應該產生GC呢?我們能否通過資源製作上的調整來避免這樣的GC呢?如下圖,因為一次性產生若干MB的GC在直觀感受上還是很可觀的。
請輸入圖片描述

準確的說這些 GC Alloc 並不是由Instantiate 直接引起的,而是因為被例項化出來的元件會進行 OnEnable 操作,而在 OnEnable 操作中產生了 GC,比如以上圖中的函式為例:
上圖中的 Text.OnEnable 是在例項化一個 UI 介面時,UI 中的文字(即 Text 元件)進行了 OnEnable 操作,其中主要是初始化文字網格的資訊(每個文字所在的網格頂點,UV,頂點色等等屬性),而這些資訊都是儲存在陣列中(即堆記憶體中),所以文字越多,堆記憶體開銷越大。但這是不可避免的,只能儘量減少出現次數。
因此,我們不建議通過 Instantiate/Destroy 來處理切換頻繁的 UI 介面,而是通過 SetActive(true/false),甚至是直接移動 UI 的方式,以避免反覆地造成堆記憶體開銷。


四、載入相關

Q1:UGUI的圖集操作中我們有這麼一個問題,載入完一張圖集後,使用這個方式獲取其中一張圖的資訊:assetBundle.Load (subFile, typeof (Sprite)) as Sprite; 這樣會複製出一個新貼圖(圖集中的子圖),不知道有什麼辦法可以不用複製新的子圖,而是直接使用圖集資源 。
請輸入圖片描述

經過測試,這確實是 Unity 在 4.x 版本中的一個缺陷,理論上這張“新貼圖(圖集中的子圖)”是不需要的,並不應該載入。 因此,我們建議通過以下方法來繞過該問題:
在 assetBundle.Load (subFile, typeof (Sprite)) as Sprite; 之後,呼叫
Texture2D t = assetBundle.Load (subFile, typeof (Texture2D)) as Texture2D;
Resources.UnloadAsset(t);
從而解除安裝這部分多餘的記憶體。

Q2:載入UI預製的時候,如果把特效放到預製裡,會導致載入非常耗時。怎麼優化這個載入時間呢?

UI和特效(粒子系統)的載入開銷在多數專案中都佔據較高的CPU耗時。UI介面的例項化和載入耗時主要由以下幾個方面構成:

  1. 紋理資源載入耗時
    UI介面載入的主要耗時開銷,因為在其資源載入過程中,時常伴有大量較大解析度的Atlas紋理載入,我們在之前的Unity載入模組深度分析之紋理篇有詳細講解。對此,我們建議研發團隊在美術質量允許的情況下,儘可能對UI紋理進行簡化,從而加快UI介面的載入效率。
    請輸入圖片描述

  2. UI網格重建耗時
    UI介面在例項化或Active時,往往會造成Canvas(UGUI)或Panel(NGUI)中UIDrawCall的變化,進而觸發網格重建操作。當Canvas或Panel中網格量較大時,其重建開銷也會隨之較大。

  3. UI相關建構函式和初始化操作開銷
    這部分是指UI底層類在例項化時的ctor開銷,以及OnEnable和OnDisable的自身開銷。

上述2和3主要為引擎或外掛的自身邏輯開銷,因此,我們應該儘可能避免或降低這兩個操作的發生頻率。我們的建議如下:

  1. 在記憶體允許的情況下,對於UI介面進行快取。儘可能減少UI介面相關資源的重複載入以及相關類的重複初始化;

  2. 根據UI介面的使用頻率,使用更為合適的切換方式。比如移進移出或使用Culling Layer來實現UI介面的切換效果等,從而降低UI介面的載入耗時,提升切換的流暢度。

  3. 對於特效(特別是粒子特效)來說,我們暫時並沒有發現將UI介面和特效耦合在一起,其載入耗時會大於二者分別載入的耗時總和。因此,我們僅從優化粒子系統載入效率的角度來回答這個問題。粒子系統的載入開銷,就目前來看,主要和其本身元件的反序列化耗時和載入數量相關。對於反序列化耗時而言,這是Unity引擎負責粒子系統的自身載入開銷,開發者可以控制的空間並不大。對於載入數量,則是開發者需要密切關注的,因為在我們目前看到的專案中,不少都存在大量的粒子系統載入,有些專案的數量甚至超過1000個,如下圖所示。因此,建議研發團隊密切關注自身專案中粒子系統的數量使用情況。一般來說,建議我們建議粒子系統使用數量的峰值控制在400以下。
    請輸入圖片描述

Q3:我有一個UI預設,它使用了一個圖集, 我在打包的時候把圖集和UI一起打成了AssetBundle。我在載入生成了GameObject後立刻解除安裝了AssetBundle物件, 但是當我後面再銷燬GameObject的時候發現圖集依然存在,這是什麼情況呢?

這是很可能出現的。unload(false)解除安裝AssetBundle並不會銷燬其載入的資源 ,是必須對其呼叫Resources.UnloadAsset,或者呼叫Resources.UnloadUnusedAssets才行。關於AssetBundle載入的詳細解釋可以參考我們之前的文章:你應該知道的AssetBundle管理機制。


五、字型相關

Q1:我在用Profiler真機檢視iPhone App時,發現第一次開啟某些UI時,Font.CacheFontForText佔用時間超過2s,這塊主要是由什麼影響的?若iPhone5在這個介面消耗2s多,是不是問題很大?這個消耗和已經生成的RenderTexture的大小有關嗎?

Font.CacheFontForText主要是指生成動態字型Font Texture的開銷, 一次性開啟UI介面中的文字越多,其開銷越大。如果該項佔用時間超過2s,那麼確實是挺大的,這個消耗也與已經生成的Font Texture有關係。簡單來說,它主要是看目前Font Texture中是否有地方可以容下接下來的文字,如果容不下才會進行一步擴大Font Texture,從而造成了效能開銷。





原文出處:侑虎科技
本文作者:admin
轉載請與作者聯絡,同時請務必標明文章原始出處和原文連結及本宣告。

相關文章