在日常開發過程當中,類似于HUD或者是彈幕之類大量的體積小但是不斷在移動的UI,在不斷的重建的過程當中會産生大量的GC,導緻遊戲卡頓到不能玩。
今天就研究一下Unity中UGUI的繪制方法以及規則:
UGUI的源碼位址(C#部分):
下載下傳位址
利用底層API進行繪制
首先我們要搞清楚,到底UGUI是如何對UI進行繪制的。我們首先從最稀疏平常的Image開始。
Image源碼嘗試閱讀
我們首先盯上的是Image,我們發現首先,其基類為MaskableGraphic,其他的接口我們先暫時不管,并且最終我們看到其最終的基類依舊是Monobehavior。
經過閱讀,其實最主要的繪制部分其實不多,主要是對Graphic基類中幾個成員對象的重寫。
- mainTexture屬性 主要負責提供貼圖
- material屬性 主要提供材質
- OnPopulateMesh函數 主要提供頂點以及UV資訊(也就是Mesh資訊)
首先是mainTexture,我們發現就是簡單從sprite中取出Texture而已,如果沒有sprite則提供預設的白色貼圖
然後是material屬性,我們可以看到,也是非常簡單,首先判斷有沒有材質,如果沒有就提供預設材質。
最後就是比較重頭的OnPopulateMesh,這個函數的主要作用就是用于建構Mesh。
在這之前我們可以先看看VertexHelper這一個類的作用。
打開VertexHelper的源碼,我們可以很清楚看到,其實VertexHelper隻是一個臨時的容器,用于存放需要渲染的Mesh資訊。
值得注意地是其中所用到的ListPool,性能高度敏感的代碼塊,需要大量的使用對象池,這是優化代碼效率減少GC的好技巧,無論是網絡子產品還是底層的元件重用,對象池都是必不可少的!
那這個時候我們其實自己可以想到,我們的合批需要怎麼做了,在我們使用UGUI的時候其實每一個Image都是使用了PopulateMesh來進行Mesh生成之後進行合批,我們其實可以将這一步直接放在這一個函數裡面,由我們自己來重寫就可以了,這樣我們就不需要調用N次PopulateMesh之後進行合并,而隻需要調用一次自己編寫的PopulateMesh函數就可以了。
我們再看一看Graphic基類當中對該函數的編寫:
上面圖中我們可以看到,其僅僅是畫了一個正方形的Mesh而已。
具體如何通過Vertex畫Mesh其實和OpenGL中VAO EBO是一樣的,就不再贅述了。
我們回過頭來看Image當中的OnPopulateMesh:
我們會發現,當Sprite為空的時候畫Image的方式就是Graphic,與我們平時的使用方式相同。
當Sprite不為空則會通過不同的sprite類型來進行繪畫,實際上就是擷取不同的UV,通過何種方式來将貼圖繪制到Mesh上面。
我們這裡就隻看一下最簡單的sprite繪制方法,我們會看到
其實也是畫個矩形……
GetDrawingDimensions在擷取Rect,而通過GetOuterUV來獲得UV。
獲得UV的時候主要是為了将Sprite中空白的地方進行裁切,主要是通過從Native層得到的padding來計算的,具體内部如何計算我們不得而知,但是我們拿到padding值就已經可以完全算出Sprite的實際大小了。
由于我們隻是繪制Simple類型的sprite,是以對UV不需要進行比較複雜的計算,實際上在其他幾種繪制方式之中,除了OuterUV還會用到InnerUV,詳細算法也就得自己慢慢看了……
至此,我們已經大緻明白了如何通過頂點來對UI進行繪制。是時候自己實踐了。
自定義Graphic
我們建立一個新的類:MyGraphic
其中我們重寫三個對象,也就是上面提到的:
- mainTexture屬性
- material屬性
- OnPopulateMesh函數
因為自己很懶惰,是以直接将DrawingDimension函數拿過來用了。
獲得了Rect以及UV之後我們就可以像普通的Image一樣繪制出圖檔了,
當然我們也可以把其中一句AddTriangle删除,我們就可以發現,我們隻繪制了一個三角面
UGUI底層繪制規則
接下去,我比較好奇的是,OnPopulateMesh到底是什麼時候進行調用的,因為我們都知道,在UI當中消耗比較大的用于都是UI的重建操作,如果我們能夠搞明白UI重建規律的話那我們應該可以大大避免UI重建的情況。
Graphic當彙總的DoMeshGeneration調用了OnPopulateMesh
我們可以看到,當RectTransform的尺寸為0的時候就不再調用OnPopulateMesh了,與以前總結出來的優化方案相同。
下面則是對Mesh進行調整并且通過VertexHelper将資料實裝到Mesh當中最後将Mesh指派給CanvasRenderer進行渲染,這下能了解為何所有顯示元件都要帶一個CanvasRenderer了吧。
UpdateGeometry調用到了DoMeshGeneration
除了Text在字型變換的時候會進行更新以外,入口都是統一的:Graphic中的Rebuild。
Rebuild實際上是實作了ICanvasElement的接口。
當頂點資料遭到修改,則立刻會進行重建,實際上我們隻需要關心何時SetVerticesDirty會被修改就可以明白到底什麼時候UI會進行重建。
不過在此之前,我們先看看Rebuild在何時會被調用。
查找引用,發現是在CanvasUpdateRegistry中的PerformUpdate中進行調用,這個PerformUpdate是挂載在Canvas.willRenderCanvases這個事件上的,也就是Canvas進行渲染之前便會進行調用。
SetVerticesDirty被調用的情況
我們回過頭來看一看何處會修改SetVerticesDirty:
當調用了SetVerticesDirty則會修改該值并且添加到Canvas的重建清單當中。
調用該函數的地方就不少了,查找之後總共34處調用。
Text文字的以下屬性進行變化都會進行Mesh重建,是以如果有大量需要經常變動的Text就要小心了。
同樣的Image圖檔也有屬性一旦改變就會引發改變,其中我們比較常用的可能就是尺寸了,例如CD、位置修改等等,都會造成重建。
RawImage中進行修改的時候也會産生重建
當蒙版進行修改的時候也會進行重建
采用Shadow的時候也會産生重建