Unity官方的UGUI優化指南讀后總結

Unity官方的UGUI優化指南讀后總結

Unity官方的UGUI優化指南: Optimizing Unity UI

Unity UI的C#是開源的,可以從Unity’s Bitbucket repository里的 UI文件夾下找到。

Unity UI居然還有單獨的Document,驚了, Unity UI Document 1.0.0

Unity官方出的Unity在5.2版本的時候對UGUI的優化方案和方向:"Unity UGUI 的整個改進過程_(5.2啟用)"

優化指南的Unity版本:2017.1 +

參考的UGUI源碼的版本:2019.1


1、優化UGUI可以從哪些方面入手


優化指南里提出了4個方向

  1. 避免GPU進行重復的像素點渲染

    • 原文的描述是說 Fragment Shader 的利用率,因為我覺得不怎么容易理解,所以會翻譯成我自己覺得比較理解的文字,后續很多內容都會做這樣的處理,因此我這篇文章是不權威的,僅僅作為幫你找到一些方向的交流使用。

    • 這里簡單地理解就是要 盡可能地避免UI上出現過多的像素疊加, 比如說Text下方有一張Image,就是屬于這種情況。

    • 這個方向能優化的東西不是很多,主要做的是在擺UI預設體的時候,Recttransform的大小控制在【夠用就好】的情況。

  2. 避免觸發 Canvas 的 Rebuild。

    在Rebuild這一塊上,已經不是老版本那種理解了,Unity做了很大的優化,參考"Unity UGUI 的整個改進過程_(5.2啟用)"。目前觸發Rebuild問題最大的,我個人經驗,常見的有三個。

    • 大量的Graphic組件的Enable同時進行開關

    • Hierarchy上的父子層級的變化

    • LayoutGroup之類的組件的設置變更

  3. Excessive numbers of rebuilds of Canvas batches (over-dirtying)

    • Canvas batches 重建次數過多。

    • [個人理解] Canvas的數量過多,并且一次性地引發多個Canvas的Rebuild。這個我想來想去,沒想到匹配的情況。只能結合后續提到的,Canvas batches 的過程是在一個叫Renderer thread線程里進行的,如果一次性進行過多的Canvas batches,會導致這個線程“遲到”,從而延遲了給主線程回傳數據時機,會引發卡幀的情況。

  4. 減少CPU處理頂點的時間。

    • 這個主要是針對Text的,因為Text不像Image,它需要根據具體的文字生成網格,并且受Text的各種屬性影響這個過程,比如說字體的Size。
    • 由于我對Text的優化沒有任何經驗,因此本文不會提及Text的優化。但原文有提到,有興趣的可以移步過去看 ,Optimizing Unity UI

2、詳細解讀UGUI的重要(基礎)組件


Unity UI的C#是開源的,可以從Unity’s Bitbucket repository里的 UI文件夾下找到。

如果你要看下去,最好配合著源碼看看,看得到的類文件,比一串類名要讓你更投入和更容易理解一些。

Canvas組件

一個原生代碼的組件,就是用C++寫的 ,一般渠道看不到源碼,我就不懂看。不過理解它的作用就行。

Canvas,(我的理解的UI的最大渲染單位)。如果你將它理解Mesh的話,可以假設它是一張大的Mesh,由其子物體的Mesh組合而成。子物體的MeshCanvas Renderer提供。

擬物版的理解,Canvas是一個1米*1米的空白畫布,畫布區域里有12生肖的貼紙(表示12個Graphic的子物體),擺的位置亂七八糟的,每個圖案都帶著一個組件Canvas Renderer,可以理解Canvas Renderer就是貼紙的膠水,是Canvas Renderer把12生肖的圖案和畫布關聯起來的,把畫布掛在墻上就類比GPU將Canvas內容渲染到屏幕上。

問:我要Canvas來干嘛?直接把12生肖貼紙貼墻上它不香嗎?

答:

簡單的回答:就是減少Draw Call,如果 DC 都不知道的話可以先去查下資料,這個知識點很重要的。

長一點的回答:減少子物體互相打斷批處理的情況。大家都知道,UI的渲染順序,是TOP -> BUTTOM,從上到下的,那如果遇到 IMAGE->TEXT->IMAGE-TEXT-IMAGE這種情況,一個一個按順序來渲染的話,豈不是要5次DC?然后你試試,在工程里按這個順序擺放5個物體。【重點,5個物體不能重疊!】。擺放好后打開Game視圖右上角的Statas,你會看到UGUI產生了2個Batches(這里暫且把Batches等價DC,實際上兩者大部分時候是不等的)。Canvas的作用,就是節省這3個DC,也是為了節省這3個DC,導致了各種UGUI的優化的出現。(看不懂就跳過,以后會理解的)

【興趣,5個物體重疊你再看看?嘿嘿,理解了并且感興趣的話,細節可以看"Unity UGUI 的整個改進過程_(5.2啟用)"

問:Canvas Renderer 就是個膠水嗎?怎么理解啊?

答:我不知道。Canvas Renderer就像一個工具人,搜集Graphic的各種信息,然后將信息傳遞給Canvas,最終Canvas將自身的信息傳遞給GPU去渲染出來。也希望各位看官,能理解這點的話,在評論里說一下。

子Canvas

在Canvas的節點下,創建一個物體,在那個物體上掛一個Canvas的組件,這一個Canvas組件就被稱為子Canvas,就是子Canvas。

上面我提了一句,Canvas是最大的渲染單位,為什么這樣說呢?常規的情況,父子節點,父節點是包含/擁有子節點的,但是Canvas不一樣,子Canvas和Canvas是互相獨立的,兩者是渲染層面上是平等關系,互不影響互不干擾。【Canvas之間互不影響互不干擾】

子Canvas一般是用來做優化用的,Unity優化指南里,也非常推薦用子Canvas去分離一些UI元素從而達到減少Canvas遍歷子節點進行的Rebuild次數以及減少Canvas觸發Rebuild是影響的UI節點數量。

問:什么時候用子Canvas?

答:這個沒有一個固定的用法,也不應該有固定的用法,一切都要根據項目來。文章后面會有這方面的的一些講解的。

Graphic 組件

你看的到的UI元素,都是繼承自Graphic的,當然繼承鏈中間穿插了一個叫做MaskableGraphic的子類,這個子類實現了IMaskable的接口,目的是為了提供遮罩的功能,就是Mask的功能(和RectMask2D無關)。RawImage,Image和Text之類的可以看得到的東西,都是繼承自MaskableGraphic的,可以從UI源碼上看的到。

Graphic就是我們的12生肖的圖案,如果圖案的某些性質發生了改變,就會觸發Graphic自身的Rebuild函數的調用。你看源碼的話,在文件里搜索下“Dirty”和“Rebuild”,你會看到很多相關的東西,這些都和Graphic的Rebuild有關。

Layout組件

每一個Reacttransform都是一個Layout,你雖然找不到Layout這個類,但萬惡之源就是它。因為它是可以嵌套的,當一個東西可以嵌套的時候,就代表它的影響范圍不僅僅是自身。

另外LayoutGroup的相關類、ContentSizeFitter 都是Layout的性能痛點的幫兇。很多人在ScrollView里用了LayoutGroup,就很難受,痛上加痛,后面有專門講這個的優化。

CanvasUpdateRegistry類

Graphic 和 Layout的組件,都依賴CanvasUpdateRegistry來實現Rebuil。這個類是一個很關鍵的類,記錄(Track)著被標記為Dirty的Layout和Graphic組件的集合,在其(Layout和Graphic組件)關聯的Canvas調用 willRenderCanvases事件時,根據需要觸發更新。根據需要的解釋可以看源代碼。

Graphic和Layout組件的更新過程稱為Rebuild。就是你覺得12生肖的圖案要改一下,你就得把畫布從墻上拿下來,拿回工作室,重新花時間修改,然后貼到畫布上(這是Canvas的事情),再貼回墻上(貼墻是GPU的事)。這個過程被稱為Rebuild。

Graphic和Layout組件的產生了修改后,就會被設置為Dirty,之前看源碼的話應該很容易理解,Dirty之后就會被CanvasUpdateRegistry跟蹤。

Dirty標記,平時寫代碼的時候,也會也經常用。比如有10件衣服,我弄臟了衣服的一小塊地方,那我就需要洗一下臟了的那些衣服,其他的衣服我可以不洗,保留下來一直用。(一件一直干凈的衣服能穿一輩子!戰術后仰!)。

Canvas就是把包含Mesh(還有很多其他信息)的專用的一系列數據緩存下來,一直用。

Canvas 渲染細節

這部分懂的就懂,不懂的以后會懂。這里我直接用原文翻譯了,每一句都是干貨。

順便推一名博主的UGUI優化指南翻譯原文,就是翻譯的時候是分段的,看起來小難受。下面一段話就是從他那里復制過來的,我英文很一般,谷歌翻譯也不算很準。

在拼UI的時候,請記住所有由Canvas繪制的圖形都將繪制在透明隊列中。也就是說,由Unity UI生成的圖形將總是從后向前繪制并啟用alpha blend。從性能的角度來看,需要記住的重要一點是,我們繪制的多邊形圖形的所有像素都將被采樣,即使這個像素完全被其他不透明的多邊形覆蓋了(就是實際上這圖片被其他Graphic遮住了,你看不見它,它也仍然會被GPU渲染的,仍然消耗著GPU資源,這就是透明隊列必然出現的情況)。在移動設備上,這種過度的繪制很快便會引起GPU的Fragment填充率的問題。

Unity UI的圖形渲染是在Transparent隊列(透明隊列)的,因此要注意Overdraw,避免同一個像素的渲染次數過多引起GPU的填充率的性能問題。

這里就是提醒你,完全不可見的UI界面,應該把它關掉,但是!最好別亂關,用Canvas的Enable來打開和關閉界面。不過這方面在最后面會詳細說。


3、Canvas的渲染過程 ( The Batch building process)


Canvas的批處理過程(The Batch building process)

關于The Batch building process,我沒能找到很詳細的過程,只是從"Unity UGUI 的整個改進過程_(5.2啟用)"找到了一點相關的內容,這個批處理過程,實現了我們[Canvas組件]這一章里提到的,在無重疊的情況降低DC數量的功能,這個功能官方的名稱叫做批處理排序。而在改進之后,這部分的性能消耗被大大地優化了,并且放到了多線程去處理,對主線程的影響相當的小。

順著往下說,我們其實很容易發現一個事情,我們在運行時移動Image的位置時,應該是打亂的舊的批處理排序,但是Profile上看性能并沒有大的波動。移動位置會將Canvas設置為Dirty,但這個Dirty是會觸發The Batch building process,而不會觸發Rebuild的,因為Rebuild是從下而上的。

聊天的時候,會把Canvas的Batch building process這一個過程稱為Rebatch,跟Rebuild區分開來。

所以對于UGUI的性能分析,要分開兩點

  • Canvas的批處理過程 Rebatch (The Batch building process)

  • GraphicLayoutRebuild

這兩點,都會影響性能。但是Rebatch是有多線程的加持的,而Rebuild是在主線程的。

  • Rebuild自身會有性能消耗,同時Rebuild會觸發Rebatch。

  • Rebatch除了被Rebuild觸發,還會被其他情況觸發。

  • Rebatch的性能上的問題,在電腦上比較難看的出來,因為有多線程加持。

(原文)計算批次需要按深度對網格進行分類,并檢查它們是否存在重疊,共享的材料等。此操作是多線程的,因此在不同的CPU架構之間,尤其是在移動SoC(通常具有很少的CPU內核)和現代臺式機CPU(通常具有4個或更多內核)之間,其性能通常會有很大差異。

感慨:我以前一直以為,不管是Rebuild還是Rebatch,最終影響性能的地方都是Canvas的Rebatch,因為以前很多人都說XXX操作會引起Canvas的Rebuild(當初還是5.x的時代)。現在面對 Rebatch + Rebuild,有點懵,想了好久,然后又做了測試才想清楚了。

回到正題:

批處理構建過程,不僅有批處理排序,還有網格合并之類的。

批處理構建過程,其中Canvas組合了其包含的UI元素的網格并生成適當的渲染命令以發送到Unity的圖形管道。此過程的結果將被緩存并重復使用,直到將Canvas標記為臟的為止(每當對其組成網格之一進行更改時都會發生)。

Canvas使用的網格取自其子節里的Graphic組件的一組Canvas Renderer組件,但不包含在任何一組Sub-Canvas的子節點下的Graphic組件。

根據上述的信息,有以下優化方案

  • 減少Rebuild,尤其是Layout的Rebuild,影響范圍大。

  • 減少由于Rebuild觸發大規模Canvas的Rebatch。優化方法使用子Canvas的動靜分離。

  • 減少大規模Canvas的Rebatch,比如部分節點有各種循環動畫引起大規模Canvas的Rebatch。優化方法使用子Canvas的動靜分離。

CanvasUpdateRegistry 引導Rebuild(The rebuild process)

CanvasUpdateRegistry 前文也有提到,應該都比較眼熟吧。打開UI的源碼,這次來仔細看它里面的一個函數,叫做PerformUpdate,查找這個函數的引用,可以看到下方這行代碼

        protected CanvasUpdateRegistry()
        {
            Canvas.willRenderCanvases += PerformUpdate;
        }

PerformUpdate函數會在WillRenderCanvases事件觸發時調用,這就是為什么我們看Profile的時候,出現性能高峰的總是會看到WillRenderCanvases的原因了。

PerformUpdate的運行過程分3步

  • 順序遍歷調用Dirty的Layout組件的Rebuild函數

  • 要求任何已注冊的剪切組件(例如蒙版)剔除所有剪切的組件。這是通過ClippingRegistry.Cull完成的。

  • 順序遍歷調用Dirty的Graphic組件的Rebuild函數

這里再次強調一下,Rebuild指的是LayoutGraphic的Rebuild,并不是Canvas的Rebatch。

private void PerformUpdate()
        {
            UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
            CleanInvalidItems();

            m_PerformingLayoutUpdate = true;
            //Layout Rebuild
            m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
            for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
            {
                for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
                {
                    var rebuild = instance.m_LayoutRebuildQueue[j];
                    try
                    {
                        if (ObjectValidForUpdate(rebuild))
                            rebuild.Rebuild((CanvasUpdate)i);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e, rebuild.transform);
                    }
                }
            }

            for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
                m_LayoutRebuildQueue[i].LayoutComplete();

            instance.m_LayoutRebuildQueue.Clear();
            m_PerformingLayoutUpdate = false;

            // now layout is complete do culling...
            ClipperRegistry.instance.Cull();
            //Graphic Rebuild
            m_PerformingGraphicUpdate = true;
            for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
            {
                for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
                {
                    try
                    {
                        var element = instance.m_GraphicRebuildQueue[k];
                        if (ObjectValidForUpdate(element))
                            element.Rebuild((CanvasUpdate)i);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
                    }
                }
            }

            for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
                m_GraphicRebuildQueue[i].GraphicUpdateComplete();

            instance.m_GraphicRebuildQueue.Clear();
            m_PerformingGraphicUpdate = false;
            UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
        }

問:為什么要Rebuild?

答:原本畫布上是12生肖從左到右按順序排的,有一天策劃讓你倒過來排,你是不是得先拿回工作室重新排下順序?這就類比Layout的Rebuild。有一天策劃讓你把12生肖換成12星座,你是不是同樣得拿回工作室重新換貼紙?這就類比Graphic的Rebuild。

問:Rebuild消耗的是什么性能資源?

答:消耗的是主線程的CPU資源。但之前也說了,Rebuild是會觸發Rebatch的,在多線程不友好的低端機器上,Rebatch的消耗并不低的。

Layout的Rebuild

要重新計算一個或多個Layout組件中包含的組件的適當位置(和可能的大小),必須按其適當的層次結構順序應用Layouts。在GameObject層次結構中靠近根部的布局可能會更改可能嵌套在其中的任何布局的位置和大小,因此必須首先進行計算。

為此,Unity UI將臟布局組件的列表按其在層次結構中的深度進行排序。層次結構中較高的項(即,父節點較少)被移到列表的前面。

然后,請求布局組件的排序列表以重建其布局;這是實際更改由布局組件控制的UI元素的位置和大小的地方。有關各個元素的位置如何受布局影響的更多詳細信息,請參見《 Unity手冊》的“ UI自動布局”部分。

這里原文講的很好很細了,也比較容易理解。

問:ContentSizeFitter是子物體反向影響父物體的,和Rebuild的順序反過來,不會沖突嗎?

答:ContentSizeFitter的變化,是在Rebuild操作之前。并且ContentSizeFitter是從子節點影響父節點的Layout大小的,就是可以理解為在Rebuild之前,受影響的父子節點的Layout的大小都已經確定了,和沒有ContentSizeFitter組件的物體沒什么區別。

Graphic Rebuild

重建圖形組件后,Unity UI會將控制權傳遞給ICanvasElement接口的Rebuild方法。圖形實現了這一點,并在“重建”過程的“預渲染”階段運行兩個不同的重建步驟。

  • 如果頂點數據已標記為臟數據(例如,當組件的RectTransform的大小更改時),則將重新構建網格。
  • 如果將材質數據標記為臟(例如,當更改組件的材質或紋理時),則將更新附加的Canvas Renderer的材質。

圖形重建不會以任何特定順序遍歷圖形組件列表,并且不需要任何排序操作。

問:單個物體的Rubuild好像消耗并不大?

答:確實,我覺得還是得益于CPU多核的發展,讓Rubuild引發的Canvas的Rebatch在多線程下得到非常好性能優化。但如果觸發Rebuild的物體個數數量多的時候,其自身的CPU資源消耗累加起來是很大的,尤其是Layout這種影響范圍很廣的Rebuild。

4、性能優化工具

Unity Profiler

沒用過的可以百度搜索下,很簡單的。

這里我照搬原文了,工具流沒啥好說的。

img

Canvas.SendWillRenderCanvases 包含對Canvas組件的willRenderCanvases 事件訂閱的C#腳本的調用。Unity UI的CanvasUpdateRegistry 類接收到此事件,并使用它來運行重建過程

注意:為了更輕松地查看UI性能的差異,通常建議禁用除“渲染”,“腳本”和“ UI”之外的所有跟蹤類別。這可以通過單擊CPU使用情況分析器左側跟蹤類別名稱旁邊的彩色框來完成。通過單擊并向上或向下拖動類別名稱,也可以在CPU事件探查器中對類別進行重新排序。

img

在2017.1及更高版本中,還有一個新的UI Profiler。默認情況下,此探查器是探查器窗口中的最后一個探查器。UI專用的視圖,看性能和合批的時候挺有用的。

img

缺陷(重要):

不幸的是,部分UI更新過程未正確分類,因此在查看UI曲線時要小心,因為它可能不包含所有與UI相關的調用。例如,Canvas.SendWillRenderCanvases 歸類為“ UI”,而Canvas.BuildBatch 歸類為“其他”和“渲染”。

Unity FrameDebugger

幀調試器,誰用誰知道,簡單得很,看得懂英文,會按鍵盤的上箭頭和下箭頭兩個按鍵就行了。

Xcode的方法,可以去原文看Optimizing Unity UI


5、從像素疊加的方面來優化UI


修復 Fill-Rate(GPU 像素填充率[過高]) 的問題

可以采取兩種措施來減輕GPU片段管道上的壓力:

  • 降低片段著色器的復雜性。有關更多詳細信息,請參見“ UI著色器和低規格設備”部分。
  • 減少必須采樣的像素數。
    由于UI著色器通常是標準化的,所以最常見的問題就是過度使用填充率。這最常見是由于大量重疊的UI元素和/或具有占據屏幕重要部分的多個UI元素。這兩個問題都可能導致透支水平過高。

簡單得來說就是要降低像素疊加的情況。

關閉不可見的UI界面

如果你打開了一個新的UI界面,是全屏的且完全遮住的舊的UI界面,可以禁用置于全屏UI下方的全部UI元素。 最簡單的方式就是把UI界面的根節點的物體設置為False,但注意不要在同一幀里處理打開和關閉,也不要在同一幀里一次性關閉,分幀處理是一個好的方法。

其次,一個另外的解決方式,會更好,使用要關閉的UI界面的最頂層的Canvas的Enable = false的方式來禁用,后文會詳細講解這個。

簡化UI的結構(Simplify UI structure

it is important to keep the number of UI objects as low as possible

  • 精簡你的UI結構,能用一張Image解決的事情,盡量不要用兩張。

  • 盡量不要創建空節點。

  • 能用材質球和Shader解決的炫酷效果,盡量不要用PS里的圖層疊加的方式來實現。

  • 因為無論是Rebuild還是Rebatch,都是UI的物體越少越好。

關閉渲染內容不可見的世界相機:

前文說了,UI是渲染在透明隊列上的。即使你打開了一個全屏的UI,在這個UI的下方,你看不見的東西,你的游戲的世界空間相機仍將在UI后面渲染標準3D場景,它仍然被渲染,仍然消耗的CPU和GPU的資源。

當打開全屏UI的時候,分3種情況吧。

  • 完全全屏的UI:這個時候可以通過關閉世界相機來輕松的減輕GPU的壓力,讓它休息下。

  • 部分不可見之背景不會動:在打開UI之后,關閉世界之前,將場景渲染為一張Texture,然后可以用RawImage等方法顯示這張Texture,制作一個假的背景。

  • 部分不可見之背景會動:許多“全屏”用戶界面實際上并不會掩蓋整個3D世界,而是使世界的一小部分可見。在這些情況下,最好只捕獲渲染紋理中可見的世界部分。利用副本相機(世界相機的仿制品)實時渲染一張RenderTexture,然后把這個RenderTexture現在在界面的“可見區域”。和第二點不一樣的是,第二點只需要渲染一次,而這個是實時渲染,沒幀都會選染,好處是渲染的物體少了很多。

注意:如果一個Canvas被設置為“Screen Space – Overlay”,那么不管場景里有幾個相機(即使是0個),Canvas都會被繪制。

分層式的UI組合

以下是原文翻譯

對于UI設計師來說,使用各種背景、元素組成一個最終UI是很常見的一件事。這樣做相對簡單,而且對迭代非常友好。但是由于UnityUI使用了透明渲染隊列,所以它的性能很糟。

想想看一個具有背景、按鈕和按鈕上的一些文本的簡單UI。由于透明隊列中的對象是從后往前排序的,在一個像素落在一個文本字形內的情況下,GPU必須采樣背景的紋理,然后是按鈕的紋理,最后是文本圖集的紋理,總共三個樣本。隨著UI復雜性的增加,更多的裝飾元素被添加到背景中,樣本的數量會迅速增加。

如果發現一個大型UI的填充率達到瓶頸,最好的辦法是創建專門的UI sprites,將UI所有的裝飾/不變元素合并到它的背景紋理中。這樣就減少了必須層疊在一起才能實現設計的元素,但這個工作量很大,而且也增加了項目圖集的大小。

這個做法也適用于子元素上。想想看一個帶有滾動商品列表的商店UI。每個商品UI元素都有一個邊框、一個背景和一些表示價格、名稱和其他信息的圖標。

商店UI需要一個背景,但是因為它的商品在背景上滾動,商品元素不能合并到商店UI的背景貼圖上。但是,商品UI元素的邊框、價格、名稱和其他元素可以合并到商品的背景上。根據圖標的大小和數量,可以節省相當多的填充率。

這種優化方式有幾個缺點,這種定制的元素不能再被重用,并且需要美術同學幫忙修改。添加大的新貼圖可能會顯著增加保存UI貼圖所需的內存數量,特別是在UI貼圖沒有按需加載和卸載的情況下。

我之前有個項目其實是利用了這一點做優化的,是一個ScrollView里面,Item的格式如下(靈魂畫手,原圖沒空找了)


image-20200704120120883.png

有4種情況,無黃色星 => 3個黃色星,按照以往的做法,就是背景框一個Image,3個星星各自一個Image。但是通過上面的優化,可以轉換為只需要一個Image組件,讓美術把4種類型的效果都做成圖片。

我這個ScrollView一次會顯示50+個這樣的Item,4個Image減少為1個,就相當于少了150+的UI物體,可以說是很大的優化了。

當然,在實際應用上,這種方法會額外增加的圖集的大小,內存的大小,有時候性能優化提升了一小截,內存增大一大截,也是不好的。內存和性能經常是互相制衡的,需要自己摸索。

用簡易的UI-Shader來優化

UI-Default定義了很多功能,如果不需要的話,可以修改一版Shader,用更簡單的Shader來渲染。普通情況比較少用,深入摳性能的話可能會用上。不過我覺得會用上這種方式的人,本身肯定是會性能優化有比較多的了解的大牛了(我自己不會用)。不理解的也沒關系,跳過就行。

//推薦的Shader
Shader "UI/Fast-Default" 
{
    Properties 
    { 
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} 
        _Color ("Tint", Color) = (1,1,1,1) 
    }
    SubShader
{
    Tags
    { 
        "Queue"="Transparent" 
        "IgnoreProjector"="True" 
        "RenderType"="Transparent" 
        "PreviewType"="Plane"
        "CanUseSpriteAtlas"="True"
    }

    Cull Off
    Lighting Off
    ZWrite Off
    ZTest [unity_GUIZTestMode]
    Blend SrcAlpha OneMinusSrcAlpha

    Pass
    {
    CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"
        #include "UnityUI.cginc"

        struct appdata_t
        {
            float4 vertex   : POSITION;
            float4 color    : COLOR;
            float2 texcoord : TEXCOORD0;
        };

        struct v2f
        {
            float4 vertex   : SV_POSITION;
            fixed4 color    : COLOR;
            half2 texcoord  : TEXCOORD0;
            float4 worldPosition : TEXCOORD1;
        };

        fixed4 _Color;
        fixed4 _TextureSampleAdd;
        v2f vert(appdata_t IN)
        {
            v2f OUT;
            OUT.worldPosition = IN.vertex;
            OUT.vertex = mul(UNITY_MATRIX_MVP, OUT.worldPosition);

            OUT.texcoord = IN.texcoord;

            #ifdef UNITY_HALF_TEXEL_OFFSET
            OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
            #endif

            OUT.color = IN.color * _Color;
            return OUT;
        }

        sampler2D _MainTex;
        fixed4 frag(v2f IN) : SV_Target
        {
            return (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
        }
    ENDCG
    }
}

擴展問題:Stencil 到底做了什么?【等我學會Shader,沒忘記的話再回頭回答】


6、UI Canvas 的網格信息刷新優化


Canvas 的Rebatch

原文用的是 UI Canvas rebuilds 的標題,我覺得有點容易混淆,因為實際上它講的概念還是Rebatch的這部分概念。

干貨很多,我直接復制原文翻譯

為了更好地顯示UI,UI系統必須為屏幕上表示的每個UI組件構造圖形。這這包括運行動態布局代碼,生成多邊形來表示UI文本字符串中的字符,并將盡可能多的圖形合并到單個網格中以最小化draw calls。

Canvas重繪可以成為性能問題的兩個主要原因:

  • 如果Canvas上可繪制UI元素的數量很大,那么計算合批本身就非常消耗性能。這是因為對元素進行排序和分析的成本與Canvas上可繪制UI元素的數量成正比。
  • 如果Canvas經常被標記為dirty(經常變化),那么可能會花費過多的時間因為一點點改動而刷新整個Canvas。
    隨著Canvas上元素數量的增加,這兩個問題都變得越來越嚴重。

重要提示:當Canvas上的任何可繪制UI元素發生更改時,畫布必須重新進行一遍合批過程。這個過程重新分析Canvas上的每個可繪制的UI元素,不管它是否已經更改。注意,“更改”是影響UI對象外觀的任何更改,包括替換sprite、修改位置和大小、修改文本網格中包含的文本等。

說實話我覺得,這部分的信息有點容易讓人誤會,容易讓人覺得UI元素發生改變,都是由于Canvas的重繪引起的。但在Profiler上看,其實UI自身的Rebuild,尤其是Layout的Rebuild消耗挺大的。

看你們選擇怎么理解吧,我還是保留觀點,Rebatch+Rebuild。Rebatch在CPU的多核多線程的加持下,在UI數量合理的情況下,越來越不會拖后腿。而Rebuild在主線程上跑,也同樣是會影響到性能的。

渲染排序優化

回想起文章的開頭做的那個測試,5個IMAGE->TEXT的那個測試,可以看到通過Canvas的批處理順序優化后,減少了3個DC。但前提是它們不重疊。因此這一小節就是提醒這么一件事。

兩個Material不相同的Graphic組件們,如果它們在Hierarchy上的層級是互相穿插的,在搭建UI的時候,保證美術效果的前提下,盡可能地不要讓他們出現疊加的情況,各自守好自己的三分兩畝地,滿足Canvas的優化條件,皆大歡喜。

這種情況,在結點層級很多的時候,需要更加注意,主要還是靠感覺和經驗。

我覺得這個其實不能稱為優化,而是UI搭建的基礎知識,必須要知道的知識點,而且實際上也很少出現穿插的情況吧?

Canvas動靜分離

以前老版本UGUI的時候,經常會提起的東西。現在其實還好,因為Rebatch被優化過了。

下面的原文谷歌翻譯

由于Canvas會在其組成的可繪制組件中的任何一個發生更改的任何時候重新繪制,因此通常最好將任何不重要的Canvas分成至少兩個部分。此外,如果希望元素同時更改,則最好嘗試將元素共置在同一Canvas上。例如進度條和倒數計時器。它們都依賴于相同的基礎數據,因此將需要同時進行更新,因此應將它們放置在同一Canvas上。

在一個畫布上,放置所有靜態且不變的元素,例如背景和標簽。第一次顯示“畫布”時,它們將批處理一次,然后不再需要重新批處理。

在第二個Canvas上,放置所有“動態”元素-那些經常變化的元素。這將確保此Canvas主要重新繪制臟元素。如果動態元素的數量變得非常大,則可能有必要將動態元素進一步細分為一組不斷變化的元素(例如,進度條,計時器讀數,動畫等)和一組僅偶爾變化的元素。

實際上,這實際上是相當困難的,尤其是在將UI控件封裝到Prefab中時。相反,很多情況下,都是在同一個一個UI界面下,通過建立子Canvas的方式來進行動靜分離。而不是像上面說的,整體分成兩個Canvas。

我個人的感覺,如果說UI界面復雜度比較高的,用動靜分離是很好的方式,我說的是用子Canvas動靜分離。至于那個整體UI動靜分離的,再見,打擾了,我這就走,3連送上好嗎。

Graphic Raycaster

這章原文也是很多干貨啊,講了原理,

是的,是你了,熟悉的原文翻譯

Graphic Raycaster是一個相對簡單的實現,它遍歷所有將' Raycast Target '設置為true的Graphic組件。每一個Raycast Target都會被進行測試。如果一個Raycast Target通過了所有的測試,那么它就會被添加到“被命中”列表中。

射線響應的要求:

  • Raycast Target處于激活狀態(active )并且是啟用了自身的(enabled)的,同時具備了Graphic組件
  • 輸入的點擊位置位于附加了Raycast Target的對象的RectTransform的Rect范圍內(注意,這里不是圖片或者Text的可視范圍哦)
  • 如果Raycast Target對象或者它的子對象中包含ICanvasRaycastFilter組件,則該Raycast Filter組件允許進行Raycast。(這部分可以在源碼找,可以看到有哪些ICanvasRaycastFilter組件)

被命中的Raycast Target列表根據深度排序,并進行過濾,以確保在相機后面的元素(即在屏幕上不可見)的被刪除掉。

如果將Graphic Raycaster的“Blocking Objects”屬性打開,Graphic Raycaster就可以將射線投射到3D或2D物理系統中。(在腳本中,屬性名為blockingObjects)。意思就是打開這個選項之后,射線就會被3D或2D物體擋住,影響對UI的點擊。

優化方向

  • 最佳做法是僅在必須接收指針事件的UI組件上啟用“ Raycast Target”設置。射線廣播目標的列表越小,必須遍歷的層次越淺,則每個射線廣播測試將更快。

  • 對于具有多個必須響應指針事件的可繪制UI對象的復合UI控件(例如希望其背景和文本都改變顏色的按鈕),通常最好將 [單個(Single)] Raycast Target放置在復合UI的根部控制。當單個Raycast Target接收到指針事件時,它可以將事件轉發到復合控件中的每個感興趣的組件。

    第二點比較難理解,這里的轉發功能,我感覺涉及到了自定義UI的方式,因為原Button組件都是只綁定了一個Graphic來接受點擊的效果變化。但我們可以通過繼承Button類來寫自己的MyButton來擴展來實現。不過很多時候也沒這個需求,所以我換一個不需要思考那么多的說法在下面。

  • 如果你的復合UI的 [最頂層] 的Graphic組件的Recttransform的Rect大小,已經滿足了你的射線檢測需要的Size大小,那么請只開啟[最頂層] 的Graphic 的射線檢測。

問:射線檢測為什么消耗那么大?

答:原文谷歌翻譯

搜索射線廣播濾鏡時,每個圖形射線廣播都將遍歷 Transform層次結構一直到根。此操作的成本與層次結構的深度成比例線性增長。必須測試與層次結構中的每個Transform 關聯的所有組件,以查看它們是否實現ICanvasRaycastFilter,因此這不是一個便宜的操作。

有幾個使用ICanvasRaycastFilter的標準Unity UI組件,例如CanvasGroupImageMaskRectMask2D,因此不能輕易消除這種遍歷。

因此,不要把Raycast藏得太深,這個理論匹配上了優化方向的第二點和第三點。

子Canvas的OverrideSorting屬性

子畫布上的overrideSorting屬性將導致Graphic Raycast測試停止爬升變換層次結構。如果可以啟用它而不會引起排序或射線廣播檢測問題,則應使用它來減少射線廣播層次結構遍歷的成本。

嗯,又是一條提示你子Canvas的重要性的信息。如果你的Raycast的物體結點層次非常的深,可以用這種方式。但是我覺得改良一下節點層次可能會更好。

ScrollView 的優化

這里我直接講優化的方式吧。

優化的原理

  • 避免Layout的Rebuild

  • 盡可能地減少onTransformParentchanged

最基礎的工作,對象池 + 無限循環滾動

對象池都不做的,那沒救了,別優化了,去搞一個對象池吧,很簡單的。后續的優化都是基于對象池的。

使用LayoutGroup的ScrollView優化

這種方法比較常見,因為比較省事。

  • 核心原理:避免LayoutGroup的屬性和其物體的Layout發生改變。因為LayoutGroup發生改變,它的Rebuild會引發其所有子物體的Layout的Rebuild。

占茅坑法:

預先在ScrollView的Content里放置好N個空物體,稱為替身,替身的大小和原Item的大小一致,替身的數量和你預備要放的物體的個數一致(你要放2000個?這...下個方案見)。滾動的時候,只需要把可見的物體作為對應的替身的子物體即可,循環回收再利用。這里優化的地方在于,你原版的Item的加入和移除,并不會引起整個ScrollView的Rebuild,只會引發你當前的Item的Rebuild。

不使用LayoutGroup的ScrollView優化(推薦)

不使用LayoutGroup,通過代碼計算每個Item的位置,自己計算并且擺放好Item所在的位置。這些操作需要自己去實際寫代碼,去感悟,我很難把難點說出來,而且難點也不難,試錯幾次就行了。這個的好處是不需要創建替身,替身數量多了之后也會到來麻煩的。

當然你熟悉了之后,還有很多種方法,只要效果好就行了。


7、其他優化


這部分照搬了,沒啥感悟的,懂了就懂了。

基于RectTransform的布局

布局組件比較昂貴,因為布局組件每次標記為臟時必須重新計算其子元素的大小和位置。(有關詳細信息,請參見基礎步驟的“圖形重建”部分。)如果給定布局中元素的數量相對較小且固定,并且布局具有相對簡單的結構,則可以用RectTransform替換布局基于布局。

通過分配RectTransform的錨點,可以根據RectTransform的父級縮放其位置和大小。例如,可以使用兩個RectTransforms實現簡單的兩列布局:

  • 左列的錨點應為X:(0,0.5)和Y:(0,1)

  • 右列的錨點應為X:(0.5,1)和Y:(0,1)

RectTransform的大小和位置的計算將由Transform系統本身以原生代碼(C++)驅動。通常,這比依賴Layout系統的性能更高。

禁用畫布

當顯示或隱藏UI的離散部分時,通常在UI的根部啟用或禁用GameObject。這樣可以確保禁用的UI中沒有任何組件接收輸入或Unity回調。

但是,這也會導致Canvas放棄其VBO數據。重新啟用畫布將需要畫布(和所有子畫布)運行重建和重新批處理過程。如果這種情況經常發生,則CPU使用率增加會導致應用程序的幀速率停頓。

一種可能但很棘手的解決方法是將要顯示/隱藏的UI放置在其自己的Canvas或Sub-canvas上,然后僅在此對象上啟用/禁用Canvas組件。

這將導致不繪制UI的網格,但它們將保持駐留在內存中,并保留其原始批處理。此外,在UI的層次結構中不會調用OnEnableOnDisable回調。

但是請注意,這不會禁用隱藏UI中的任何MonoBehaviour,因此這些MonoBehaviours仍將接收Unity生命周期回調,例如Update。

為避免此問題,將以這種方式禁用的UI上的MonoBehaviours不應直接實現Unity的生命周期回調,而應從UI根GameObject上的“回調管理器” MonoBehaviour接收其回調。每當顯示/隱藏UI時,都可以通知此“回調管理器”,并且可以確保根據需要傳播或不傳播生命周期事件。此“回調管理器”模式的進一步說明不在本指南的范圍之內。

分配事件攝像機

如果將Unity的內置輸入管理器與 Canvas 的 Render Mode 設置為在 *World Space* or *Screen Space – Camera*模式中使用,則始終分別設置“事件攝像機”或“渲染攝像機”屬性非常重要。Canvas的類屬性上,統一叫做worldCamera

image-20200704133403795.png

如果未設置此屬性,則Unity UI將通過使用Main Camera標簽查找附加到GameObjects的Camera組件來搜索主攝像機。每個世界空間或攝影機空間畫布將至少進行一次此查找。由于GameObject.FindWithTag的運行速度很慢,因此強烈建議所有World Space和Camera Space畫布在設計時或初始化時分配其Camera屬性。

對于 Overlay Canvases模式的 Canvas 不會發生這個問題。

UI源代碼定制

UI系統已經開源啦。這種靈活性很棒,但這也意味著在不破壞其他功能的情況下就無法輕松進行某些優化。如果最終遇到的情況是可以通過更改C#UI源代碼獲得一些CPU周期,則可以重新編譯UI DLL并覆蓋Unity隨附的UI DLL。此過程記錄在Bitbucket存儲庫的自述文件中。確保獲取與您的Unity版本相對應的源代碼。

但是,由于存在一些重要的缺點,因此只能作為最后的手段。首先,您必須找到一種將新DLL分發給開發人員并構建計算機的方法。然后,每次升級Unity時,都必須將更改與新的UI源代碼合并。確保您不能僅僅擴展現有的類或編寫自己的組件版本,然后再朝該方向發展。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379