首先貼出官方文檔。
一、基礎概念
DrawCall
即繪制調用命令,CPU在準備好渲染數(shù)據(jù)并設置渲染狀態(tài)后,會通過Drawcall命令通知GPU進行渲染。
Canvas
Canvas是一個 Native 層實現(xiàn)的Unity組件,被 Unity 渲染系統(tǒng)用于在游戲世界空間中渲染分層幾何體(layered geometry)。
Canvas 負責把它們包含的Mesh合批,生成合適的渲染命令發(fā)送給 Unity 圖形系統(tǒng)。以上行為都是在Native C++代碼中完成,我們稱之為 Rebatch 或者Batch Build,當一個 Canvas 中包含的幾何體需要Rebacth時,這個Canvas就會被標記為Dirty狀態(tài)。
Canvas 組件可以嵌套在另一個 Canvas 組件下,我們稱為子Canvas,子 Canvas 可以把它的子物體與父Canvas分離,使得當子Canvas被標記為Dirty時,并不會強制讓父 Canvas 也強制 Rebuild,反之亦然。但在某些特殊情況下,使用子Canvas進行分離的方法可能會失效,例如當對父Canvas的更改導致子Canvas的大小發(fā)生變化時。
可以在Profiler中通過查看標志性函數(shù) Canvas.BuildBatch 的耗時,來了解 Rebatch 的性能消耗。
Canvas Renderer
幾何體(layered geometry)數(shù)據(jù)是通過 Canvas Renderer 組件被提交到 Canvas 中。
VertexHelper
頂點輔助類,用于保存UI的頂點、顏色、法線、uv、三角形索引等信息。
Graphic
Graphic 是UGUI的C#庫提供的一個基類。它是為Canvas提供可繪制幾何圖形的所有UGUI的C#類的基類。大多數(shù)Unity內置的繼承 Graphic 的類都是通過繼承一個叫 MaskableGraphic 的子類來實現(xiàn),這使得他們可以通過IMaskable 接口來被隱藏。Drawable 類的子類主要是Image和Text,且UGUI已提供了同名組件。
Layout
Layout控制著RectTransform的大小和位置,通常用于創(chuàng)建復雜的布局,這些布局需要對其內容進行相對大小調整或相對位置調整。Layout僅依賴于RectTransforms,并且僅影響其關聯(lián)RectTransforms的屬性。這些Layout類不依賴于Graphic類,可以獨立于UGUI的Graphic類之外使用。
CanvasUpdateRegistry
這個單例類維護了 m_LayoutRebuildQueue 和 m_GraphicRebuildQueue 兩個重建隊列,在構造函數(shù)中監(jiān)聽了Canvas的 willRenderCanvases 事件,這個事件會在渲染前進行每幀調用。在回調函數(shù) PerformUpdate() 函數(shù)中,遍歷兩個重建隊列進行UI重建,并執(zhí)行ClipperRegistry的Cull方法。
Rebuild
Rebuild是指 Layout 和 Graphic 組件的網(wǎng)格被重新計算,這個過程在 CanvasUpdateRegistry 中執(zhí)行。
可以在Profiler中通過查看標志性函數(shù) Canvas.SendWillRenderCanvas 的耗時,來了解Mesh重建的性能消耗。
ICanvasElement
ICanvasElement接口,重建的時候會調用它的Rebuild方法,繼承它的類都會對這個函數(shù)進行重寫,Unity中幾乎所有的UI組件都繼承自這個接口。
二、UI Batching
Batching是指Canvas通過合并UI元素的網(wǎng)格,生成合適的渲染命令發(fā)送給Unity圖形渲染流水線。Batch的結果被緩存復用,直到這個Canvas被標為dirty,當Canvas中某一個構成的網(wǎng)格改變的時候就會被標記為dirty。
從CPU把數(shù)據(jù)發(fā)送到顯卡相對較慢,合批是為了一次性發(fā)送盡可能多的數(shù)據(jù)。
batch build、batching、rebatch等都是同一個概念。
計算批次需要按深度對網(wǎng)格進行排序,并檢查它們是否有重疊、以及材質和紋理貼圖是否相同等。
首先進行深度排序:按照Hierarchy窗口從上往下的順序
- 不渲染的UI元素Depth為 -1(setactive為false,canvasgroup.alpha為0,disable),UI下沒有和其他UI相交時,該UI的Depth為0。(相交指網(wǎng)格有重疊)
- 當前UI下面有一個UI與其相交,若兩者貼圖和材質相同時,它們Depth相同,否則上面的UI的Depth是下面UI的Depth+1。
- 當前UI下面與多個UI相交,則取多個UI中Depth最高的元素(Max)與當前UI比較,若兩者貼圖和材質相同,則它們Depth相同,否則Depth = Max + 1。
排序完成后對Depth,材質,貼圖都相同的UI進行合批。(C++實現(xiàn),未開放源碼)
常見的打斷合批的原因:
- 同一深度 UI 元素使用了不同的材質或貼圖,比如不同的圖集或者字體。
- 使用了Unity的默認圖片或默認字體,本質上和上面一條相同。
- 原本能夠被合批的UI在Hierarchy層級相鄰,即使Z軸不同,也能被合批。但是原本可以合批的UI的Hierarchy層級之間或下方插入了其他UI,此時如果有UI的Z坐標不為0可能會打斷合批。
- UI使用了Mask,其本身和子節(jié)點不參與外部合批。同深度、同材質、同貼圖的Mask之間可以合批,不同Mask下的子物體也可以合批。
- UI使用了RectMask2D,其子節(jié)點不參與外部合批。UI本身參與外部合批,不同RectMask2D下的子物體不能合批。
調試工具
1)通過 Frame Debug 查看每個DrawCall的繪制:
注意:UGUI的 drawcall 根據(jù)Canvas渲染模式的不同,所在的位置也有所不同:
Screen Space - Overlay 模式時,將會出現(xiàn)在 Canvas.RenderOverlays 分組。
Screen Space - Camera 模式時,將會出現(xiàn)在所選相機的 Camera.Render 分組,作為一個 Render.TransparentGeometry 子組。
World Space 渲染模式時,將會作為一個 Render.TransparentGeometry 子組,出現(xiàn)在每個可以觀察到該 Canvas 的相機下。
2)通過 Profiler 的 UI Details 欄目查看所有Canvas的合批情況、打斷合批的原因以及每個批次繪制了哪些內容:
合批優(yōu)化策略
- UI設計的時候應盡量保持UI使用相同的材質并處于同一深度(使用圖集、注意UI的遮擋關系);
- 不要使用默認圖片和默認字體;
- 特殊情況可以使用藝術字代替文本參數(shù)合批(bmfont);
- UI的Z軸統(tǒng)一設置為0;
- 如果需要使用遮罩,僅需要使用一個的時候用RectMask2D(Mask多兩個DrawCall),需要使用多個的時候使用Mask(不同Mask的子節(jié)點參與合批);
三、UI Rebuild
Rebuild分為Layout Rebuild 和Graphic Rebuild。
Layout Rebuild
要重新計算一個或者多個Layout組件所包含的UI組件的適當位置(以及可能的大小),有必要對Layout應用層次進行排序。在GameObject的hierarchy中靠近root的Layout可能會影響改變嵌套在它里面的其他Layout的位置和大小,所以必須首先計算。 為此,UGUI根據(jù)層次結構中的深度對dirty的Layout組件列表進行排序。層次結構中較高的Layout(即擁有較少的父transform)將被移到列表的前面。然后,排序好的Layout組件的列表將被rebuild,在這個步驟Layout組件控制的UI元素的位置和大小將被實際改變。關于獨立的UI元素如何受Layout組件影響的詳細細節(jié),請參閱Unity Manual的UI Auto Layout章節(jié)。 [ 這就是為什么unity的布局組件一旦形成嵌套,套內組件將失效的原因 , unity也暫時未開放布局執(zhí)行層級順序的接口 , 僅在UGUI代碼中可見但未公開 ]
Graphic Rebuild
當Graphic組件被rebuild的時候,UGUI將控制傳遞給ICanvasElement接口的Rebuild方法。Graphic執(zhí)行了這一步,并在rebuild過程中的PreRender階段運行了兩個不同的rebuild步驟:1.如果頂點數(shù)據(jù)已經(jīng)被標為Dirty(例如組件的RectTransform已經(jīng)改變大?。?,則重建網(wǎng)格。2.如果材質數(shù)據(jù)已經(jīng)被標為Dirty(例如組件的material或者texture已經(jīng)被改變),則關聯(lián)的Canvas Renderer的材質將被更新。Graphic的Rebuild不會按照Graphic組件的特殊順序進行,也不會進行任何的排序操作。
Rebuild 通常會觸發(fā) Batching。
源碼分析
1. Rebuild的執(zhí)行過程
Canvas每幀執(zhí)行。
CanvasUpdateRegistry在構造函數(shù)中監(jiān)聽并注冊回調函數(shù) PerformUpdate。
下面是 PerformUpdate() 源碼:
private void PerformUpdate()
{
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
//清理Queue中值為null或者被銷毀的元素
CleanInvalidItems();
m_PerformingLayoutUpdate = true;
//根據(jù)父節(jié)點多少排序(層級)
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
{
UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);
for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
{
var rebuild = m_LayoutRebuildQueue[j];
try
{
//布局重建
if (ObjectValidForUpdate(rebuild))
rebuild.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
UnityEngine.Profiling.Profiler.EndSample();
}
//通知布局重建完成
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue[i].LayoutComplete();
m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Render);
// now layout is complete do culling...
UnityEngine.Profiling.Profiler.BeginSample(m_CullingUpdateProfilerString);
//執(zhí)行裁剪(cull)操作
ClipperRegistry.instance.Cull();
UnityEngine.Profiling.Profiler.EndSample();
m_PerformingGraphicUpdate = true;
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);
for (var k = 0; k < m_GraphicRebuildQueue.Count; k++)
{
try
{
var element = m_GraphicRebuildQueue[k];
//圖形重建
if (ObjectValidForUpdate(element))
element.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, m_GraphicRebuildQueue[k].transform);
}
}
UnityEngine.Profiling.Profiler.EndSample();
}
//通知圖形重建完成
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue[i].GraphicUpdateComplete();
m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Render);
}
2. UI是怎么加入重建隊列的
查看源碼發(fā)現(xiàn),主要通過以下兩個函數(shù)將待重建的ICanvasElement加入重建隊列中:
一般通過臟標記來實現(xiàn),以Graphic為例:
通過 SetLayoutDirty() 觸發(fā) LayoutRebuilder.MarkLayoutForRebuild(rectTransform) 將UI加入m_LayoutRebuildQueue 重建隊列中。
通過 SetVerticesDirty()、SetMaterialDirty()、以及 OnCullingChanged() 的調用將UI加入m_GraphicRebuildQueue 重建隊列中。
通過查看源碼中哪些地方調用了這幾個函數(shù),就能知道什么情況下會觸發(fā)UI的Rebuild了。
常見觸發(fā)Rebuild的操作:
- RectTransform 的 Width,Height,Anchor,Pivot改變。
- Text 的內容及顏色變化、設置是否支持富文本、更改對齊方式、設置字體大小等。
- Image 組件顏色變化、更換Sprite。
- Slider 組件每次滑動時。
- ScrollBar 組價每次滑動時。
- SetActive、Enable為true時。
- Mask 勾選/取消勾選 Show Mask Graphic。
- Material改變。等等…
經(jīng)測試,改變Position,Rotation,Scale不會引起UI重建,但是會直接觸發(fā)Batching。
反射查看Rebuild隊列:
可以在運行時查看哪些元素引起UI重建。
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
public class LogRebuildInfo : MonoBehaviour
{
IList<ICanvasElement> m_LayoutRebuildQueue;
IList<ICanvasElement> m_GraphicRebuildQueue;
private void Awake()
{
System.Type type = typeof(CanvasUpdateRegistry);
FieldInfo field = type.GetField("m_LayoutRebuildQueue", BindingFlags.NonPublic | BindingFlags.Instance);
m_LayoutRebuildQueue = (IList<ICanvasElement>)field.GetValue(CanvasUpdateRegistry.instance);
field = type.GetField("m_GraphicRebuildQueue", BindingFlags.NonPublic | BindingFlags.Instance);
m_GraphicRebuildQueue = (IList<ICanvasElement>)field.GetValue(CanvasUpdateRegistry.instance);
}
private void Update()
{
for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
{
var element = m_LayoutRebuildQueue[j];
if (ObjectValidForUpdate(element))
{
Debug.LogErrorFormat("{0} 引起 {1} 網(wǎng)格布局重建", element.transform.name, element.transform.GetComponentInParent<Canvas>().name);
}
}
for (int j = 0; j < m_GraphicRebuildQueue.Count; j++)
{
var element = m_GraphicRebuildQueue[j];
if (ObjectValidForUpdate(element))
{
Debug.LogErrorFormat("{0} 引起 {1} 網(wǎng)格圖形重建", element.transform.name, element.transform.GetComponentInParent<Canvas>().name);
}
}
}
private bool ObjectValidForUpdate(ICanvasElement element)
{
var valid = element != null;
var isUnityObject = element is Object;
if (isUnityObject)
valid = (element as Object) != null; //Here we make use of the overloaded UnityEngine.Object == null, that checks if the native object is alive.
return valid;
}
}
3. Rebuild具體做了些什么
以Graphic為例。
3.1 圖形重建過程:
public virtual void Rebuild(CanvasUpdate update)
{
if (canvasRenderer == null || canvasRenderer.cull)
return;
switch (update)
{
case CanvasUpdate.PreRender:
if (m_VertsDirty)
{
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
UpdateMaterial();
m_MaterialDirty = false;
}
break;
}
}
-
UpdateGeometry()
Graphic中有個靜態(tài)對象s_VertexHelper保存每次生成的Mesh信息(包括頂點,三角形索引,UV,頂點色等數(shù)據(jù)),使用完后會立即清理掉等待下個Graphic對象使用。
我們可以看到,s_VertexHelper中的數(shù)據(jù)通過OnPopulateMesh函數(shù),進行填充,它是一個虛函數(shù)會在各自的類中實現(xiàn),我們可以在自己的UI類中,重寫OnPopulateMesh方法,實現(xiàn)自定義的UI。
s_VertexHelper數(shù)據(jù)填充之后,調用FillMesh() 方法生成真正的Mesh,然后調用 canvasRenderer.SetMesh() 方法來提交。SetMesh() 方法最終在C++中實現(xiàn),這也是UGUI的效率比NGUI高一些的原因,因為NGUI的Mesh合并是在C#中完成的,而UGUI的Mesh合并是在C++中底層完成的。 -
UpdateMaterial()
UpdateMaterial() 方法會通過canvasRenderer來更新Material與Texture。
3.2 布局重建過程:
LayoutRebuilder 的 Rebuild() 方法:
PerformLayoutCalculation() 方法會遞歸計算UI元素的寬高(先計算子元素,然后計算自身元素)。
ILayoutElement.CalculateLayoutInputXXXXXX() 在具體的實現(xiàn)類中計算該UI的大小。
PerformLayoutControl() 方法會遞歸設置UI元素的寬高(先設置自身元素,然后設置子元素)。
ILayoutController.SetLayoutXXXXX() 在具體的實現(xiàn)類中設置該UI的大小。
UI重建優(yōu)化策略
- 動靜分離:細分Canvas,把相對靜態(tài)的、不會變動的UI放在一個Canvas里,而相對變化比較頻繁的UI就放在另一個Canvas里。注意:新增Canvas會打斷合批,增加DrawCall。
- 隱藏界面時,可用CanvasGroup.Alpha=0,或者從Camera渲染層級里移除等方法隱藏,代替SetActive。
- 對于血條、角色頭頂名稱、小地圖標記等頻繁更新位置的UI,可盡量減低更新頻率,如隔幀更新,并設定更新閾值,當位移大于一定數(shù)值時再賦值(重復賦相同的值,也會SetDirty觸發(fā)重建)。
- 注意合理設計UI的層級,由于布局重建需要對UI進行排序,層級太深影響排序消耗。
四、OverDraw
Overdraw是指一幀當中,同一個像素被重復繪制的次數(shù)。Fill Rate(填充率)是指顯卡每幀每秒能夠渲染的像素數(shù)。在每幀繪制中,如果一個像素被反復繪制的次數(shù)越多,那么它占用的資源也必然更多。Overdraw與Fill Rate成正比,目前在移動設備上,F(xiàn)illRate的壓力主要來自半透明物體。因為多數(shù)情況下,半透明物體需要開啟 Alpha Blend 且關閉 ZTest和 ZWrite,同時如果我們繪制像 alpha=0 這種實際上不會產(chǎn)生效果的顏色上去,也同樣有 Blend 操作,這是一種極大的浪費。
不幸的是,Canvas繪制的所有幾何體都在透明隊列中繪制。也就是說,Unity UI生成的幾何體將始終使用 Alpha 混合從前向后繪制。從多邊形柵格化后的每個像素都將被采樣,即使它完全由其他不透明多邊形覆蓋。在移動設備上,這種高水平的透支可以快速超過GPU的填充率容量。
在場景【scene】下拉列表中選擇overdraw就能看見,越亮的地方就是overdraw最多的部分。
OverDraw優(yōu)化策略
- 減少UI重疊層級,隱藏處于底下被完全覆蓋的UI面板。
- 對于需要暫時隱藏的UI,不要直接把Color屬性的Alpha值改為0,UGUI中這樣設置后仍然會渲染,應該用CanvasGroup組件把Alpha值置零。
- 需要響應Raycast事件時,不要使用空Image,可以自定義組件繼承自MaskableGraphic,重寫OnPopulateMesh把網(wǎng)格清空,這樣可以響應Raycast而又不需要繪制Mesh。
- 打開全屏界面,關閉場景攝像機。對于一些非全屏但覆蓋率較高的界面,在對場景動態(tài)表現(xiàn)要求不高的情況下,可以記錄下打開UI時的畫面,作為UI背景,然后關掉場景攝像機。
- 裁掉無用區(qū)域,鏤空,對于 Sliced 類型的 Image 可以看情況取消 Fill Center。
- 保持UI上的粒子特效簡單,盡量不要發(fā)生重疊。
五、其他優(yōu)化
- 所有可點擊組件例如 Image、Text 在創(chuàng)建時默認開啟 RaycastTarget。當進行點擊操作時,會對所有開啟RaycastTarget的組件進行遍歷檢測和排序。實際上大部分的組件是不需要響應點擊事件的,對于這些組件我們應該取消RaycastTarget屬性,最好的方式是監(jiān)聽組件創(chuàng)建,在創(chuàng)建時直接賦值為 false,對于需要響應事件的組件再手動開啟。
- Text 盡量不要使用 outline 或者 shadow 組件,會使頂點數(shù)量成倍增加。字體效果考慮 Shader實現(xiàn),或者直接讓美術同學把陰影和描邊做到字體里。
六、總結
常見UI性能問題:
- DrawCall過高,合批和提交批次(CPU --> GPU )花費大量CPU時間(Rebatch)。
- UI重建花費大量CPU時間(Rebuild)。
- 填充率過高,導致GPU渲染壓力過大(overdraw)。
- 生成頂點花費大量CPU時間(通常來自文本)。
上面針對這些問題提出了一些通用的優(yōu)化策略。但正如官方文檔所說:
The core tension when optimizing any Unity UI is the balancing of draw calls with batching costs. While some common-sense techniques can be used to reduce one or the other, complex UIs must make trade-offs.
UI優(yōu)化的核心是DrawCalls和Batching開銷的平衡。可以使用一些常識性技術來減少其中之一,但復雜的UI必須在兩者間進行權衡。
舉例:
修改 Graphic 的Color屬性,其原理是修改頂點色,因此是會引起網(wǎng)格的Rebuild的(即Canvas.BuildBatch操作,同時也會有Canvas.SendWillRenderCanvases的開銷)。而通過修改頂點色來實現(xiàn)UI元素變色的好處在于,修改頂點色可以保證其材質不變,因此不會產(chǎn)生額外的DrawCall。
在UI的默認Shader中存在一個Tint Color的變量,正常情況下,該值為常數(shù)(1,1,1),且并不會被修改。如果是用腳本訪問Material,并修改其Tint Color屬性時,對UI元素產(chǎn)生的網(wǎng)格信息并沒有影響,因此就不會引起網(wǎng)格的Rebuild。但這樣做因為修改了材質,所以會增加一個Draw。
這時候就得權衡一下是要更少的DrawCall,還是減少UI的重建更合適。文章來源:http://www.zghlxwxcb.cn/news/detail-461827.html
七、參考文章
https://edu.uwa4d.com/lesson-detail/126/482/0?isPreview=false uwa drawcall rebatch rebuild particle 等
https://www.jianshu.com/p/5a39cfa74232 UI Rebuild過程詳解
https://blog.csdn.net/gaojinjingg/article/details/103565840?spm=1001.2101.3001.6650.3 Unity UGUI優(yōu)化與原理
https://www.drflower.top/posts/aad79bf1/ UGUI性能優(yōu)化總結
https://zhuanlan.zhihu.com/p/350778355 OverDraw詳解文章來源地址http://www.zghlxwxcb.cn/news/detail-461827.html
到了這里,關于Unity 性能優(yōu)化基礎的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!