iOS視圖控件的內容顯示和離屏渲染流程

 iOS中UI控件內容顯示流程

UIKit界面組成
iOS中組成頁面的各個元素基本來自UIKit,我們可以修改佈局或自定義繪製來修改UIKit元素的默認展示。
UIView的頁面顯示內容有CALayer負責,事件的接收與響應由UIView自己負責。
爲什麼需要有這樣的分工呢,原因是因爲Mac上和iPhone上的事件存在很大的區別,iPhone 是屏幕觸摸事件,Mac上是鼠標,鍵盤等事件,但是顯示上卻是高度一致的,因此把顯示部分單獨封裝成CALayer而存在來。

UIView默認是CALayer的CALayerDelegate,它負責創建並管理它的圖層,以確保當子視圖在層級關係中添加或者被移除的時候,它們關聯的圖層也同樣對應在層級關係樹當中有相同的操作。
每個View被創建的時候都會自動創建一個CALayer,同時還可以在後續的操作中添加多個layer。

CALayer有個id類型的contents屬性,它指向內存中的一個成爲backing storage的存儲空間。往contents上賦值的時候就會將UIView的顯示內容存儲到這個backing storage中,
這裏個id類型是一個兼容的寫法,它在iOS上時CGImageRef類型,在Mac OS上是NSImage類型。
如何將顯示的內容繪製到CALayer上
在創建流程中可分成兩大分支:
1.通過CALayer的Delegate繪製
簡單理解爲通過實現UIView的代理方法displayLayer:或者重寫CALayer的display方法,手動給layer.contents賦值,將內容繪製到CALayer 默認的backing store上。

在我們調用[UIView setNeedsDisplay]的時候,會觸發[view.layer setNeedsDisplay],緊接着調用[view.layer display] 在這個方法中會判斷layer.delegate 是否實現了displaylayer如果有則將layer傳遞出去,
然後在UIView的displayLayer:(CALayer *)layer方法中對contents進行賦值。注意:UIView默認爲layer.delegate。

具體案例有:SDAnimatedImageView的代理實現
- (void)displayLayer:(CALayer *)layer {
    UIImage *currentFrame = self.currentFrame;
    if (currentFrame) {
        layer.contentsScale = currentFrame.scale;
        layer.contents = (__bridge id)currentFrame.CGImage;
    }
}
另一種實現方法是在CALayer中複寫layer的display方法,在其中對contents進行賦值
具體案例有:YYTextAsyncLayer的重寫實現
- (void)display {
    super.contents = super.contents;
    [self _displayAsync:_displaysAsynchronously];
}
2.使用系統內部繪製
系統開始的時候會創建一個新的backing store,然後開始走drawInContext,這時候會先看layer.delegate是否實現了drawRect
如果有則用drawRect,
否則調用drawLayer:inContext:
並將管理新建backing store的context傳遞出來。

提交圖層樹到Render Server
UIView的顯示內容創建好之後,後面就是準備渲染了。
在一個界面從開始到提交到Render Server前一共可以分成三個步驟:
Layout
Prepare && Display
Commit
Layout
一個控件在添加到界面上時,會自動觸發佈局,從而確定整個層級數中每個控件的frame。
Prepare && Display
這部分會涉及到圖片的解碼,文本繪製,或者通過CALayer暴露出來的CGContextRef在backing store中進行繪製。
圖片解碼一般發生在Prepare階段。
存儲在backing store的 bitmap後續就會被打包送到Render Server中。
Commit
當RunLoop即將進入休眠期間或者即將退出的時候,會通過已經註冊的通知回調執行_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv函數,
在這個函數會遞歸將待處理的圖層進行打包壓縮,並通過IPC方式發送到Render Server。
這時候的Core Animation會創建一個OpenGL ES紋理並將backing store中的位圖上傳到對應的紋理中。

在將圖層樹發送到GPU之前Core Animation做的處理工作
Render Server在拿到壓縮後的數據的時候,首先對這些數據進行解壓,從而拿到圖層樹,然後根據圖層樹的層次結構,每個層的alpha值opeue值,RGBA值、以及圖層的frame值等對被遮擋的圖層進行過濾,刪除無需渲染的圖層,最終得到渲染樹,渲染樹就是指將圖層樹對應每個圖層的信息,比如頂點座標、頂點顏色這些信息,抽離出來,形成的樹狀結構。GPU收到的原始處理數據就是這課渲染樹。
然後將渲染樹發送到GPU,GPU開啓真正的渲染流程。

 

GPU的渲染流程

往細得分可以分成六個階段
頂點着色器(Vertex Shader):
在Render Server 拿到頂點數據並輸入到渲染管線的時候,頂點着色器會對每個頂點數據進行一次運算,每個頂點都對應一組頂點數組,這些數組可以用於存儲:頂點座標,RGBA顏色,輔助顏色,紋理座標以及多邊形邊界標誌等。
圖元裝配(Shape Assembly):
圖元裝配的過程就是將頂點連接起來,形成一個個所支持的圖元元素
幾何着色器(Geometry Shader):
把圖元裝配後的產物,圖元形式的一系列頂點的集合作爲輸入,來產生新頂點構造出新的圖元來生成其他形狀
光柵化(Rasterization):
光柵化會把圖元映射爲最終屏幕上相應的像素,生成供片段着色器使用的片段,OpenGL中的一個片段是OpenGL渲染一個像素所需的所有數據,它包含位置,顏色,紋理座標等信息。
裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。
片段着色器(Fragment Shader):
片段着色器的主要目的是計算一個像素的最終顏色,包括光照、陰影、光的顏色等等,這些數據可以被用來計算最終像素的顏色。
根據頂點着色器輸出的頂點紋理座標對紋理貼圖進行採樣,以計算該片段的顏色值。從而調整成各種各樣不同的效果圖。
測試與混合(Tests and Blending):
檢測片段的對應的深度值,用它們來判斷這個像素是其它物體的前面還是後面,決定是否應該丟棄。這個階段也會檢查alpha值並對物體進行混合(Blend)。
屏幕顯示器顯示原理
顯示器和GPU的關係是生產消費者關係,GPU生成要顯示的圖像數據放到幀緩衝區,顯示器從幀緩衝區讀取出來,在屏幕上展示。
顯示器的展示原理是使用電子槍從左上角到右下角逐屏掃描的。掃描槍在掃描過程中嚴格根據掃描槍信號進行掃描。
當水平信號HSync來時,掃描槍從左到右掃描一行,然後移動到下一行等待,當下個HSync到來時,重複上一個操作。
當一屏幕掃描完成,掃描槍回到左上角,等待VSync下一個垂直信號的到來。
顯示器和GPU之間使用了雙緩存機制,在顯示器顯示某幀數據的時候,GPU可以往另一個緩存中提交渲染好的數據,在VSync信號到來的時候,視頻控制器切換到另一個緩存用於顯示,如果在規定時間1/60s內沒有完成往另一個緩存中寫入要展示的數據,這時候視頻控制器就不會將緩存切換到未完成的幀,而是繼續顯示當前的內容。這就給人們帶來視覺上的卡頓。

 

 
離屏渲染
 
屏幕渲染流程有2種方式:正常渲染流程,離屏渲染流程

 

正常渲染流程
CPU通過佈局計算,文本繪製,圖片解碼,將得到的渲染樹通過Core Animation提交給GPU
GPU使用畫家算法,根據圖層距離屏幕的距離由遠到近分別對圖層進行紋理映射,頂點着色,光柵化等得到每個圖層的像素效果,再通過圖層混合,透明計算,深度計算得出可以展示的圖像信息放到幀緩存區
視頻控制器在每個垂直信號來到時,讀取幀緩存區數據在屏幕上展示,然後立即丟棄這幀數據,不做任何保留。這樣可以提高渲染性能,不同幀數據各自獨立。
離屏渲染流程
當給視圖設置圓角,陰影,蒙版這些圖層預合成屬性時,表示視圖內容(包括layer及其所有的sublayers)在其預合成之前是不能在屏幕中繪製的,即:預合成之前不能放到幀緩衝區。
因爲幀緩存區的圖層數據是用完就丟棄,本地不會記錄,所以需要開闢一個離屏緩存區用來存儲視圖中所有要處理的圖層數據,先按畫家算法由遠到近逐個將圖層渲染近緩存區,然後再按畫家算法的順序由遠到近的進行畫圓角,最後把它們合併疊加,把結果一起放到幀緩存區中。
視頻控制器在每個垂直信號來到時,讀取幀緩存區數據在屏幕上展示。

 

觸發離屏渲染的方式
1.shouldRasterize(光柵化)
2.masks(遮罩)
3.shadows(陰影)
4.edge antialiasing(抗鋸齒)
5.group opacity(不透明)
6.複雜形狀設置圓角等
7.漸變
離屏渲染的問題
增加性能消耗,可能導致掉幀,CPU從收到的渲染樹到轉成bitmap寫到幀緩衝區,這一套流水線是源源不斷的。而因爲要使用離屏渲染,則先要把每個圖層的處理結果不斷記錄在離屏緩衝區,最後還要做進行額外的處理,然後再把處理結果移動到當前幀緩衝區。這是二個流程的切換,中間要記錄上下文。這個額外的操作對於
性能消耗比較大,如果不能1/60s完成,可能造成掉幀
離屏渲染要額外開闢一內存進行預合成操作浪費內存。

離屏渲染的優點
保存中間狀態:如果一些視圖狀態不能一次性完成,則可以臨時保存中間狀態,如圓角,陰影,蒙版。
提升渲染效率:如果一個狀態要多次渲染,可以提前渲染完成放到離屏緩衝區,等屏幕展示時直接使用。如開啓光柵化

複用離屏渲染結果
shouldRasterize(光柵化)
當設置視圖的shouldRasterize = YES,開啓光柵化時,系統會將視圖離屏渲染得到的結果(如:添加了陰影,遮罩後的結果)保存到位圖中緩存起來,這裏的位圖中的元素和幀緩衝區的像素是一一對應的。
如果視圖的 layer 及其 sublayers 都沒有發生變化,則在下一幀渲染時直接拿來複用,提供了渲染效率。
光柵化是把GPU的渲染工作從GPU挪到了CPU,並將結果位圖做了緩存,等屏幕展示時,直接拿來複用。
這對視圖內容複雜,繪製起來麻煩,而又不怎麼變化的場景比較適合(它會將整個視圖作爲一張圖片進行保存,等展示時直接拿來複用),而對於經常變化需要重繪的視圖如tableViewCell,則返回會增加內存消耗,因爲TableViewCell有複用機制,Cell中的內容會經常變化。
光柵化使用建議如下:
1.如果layer不能被複用,則沒有必要開啓光柵化
2.如果layer不是靜態,需要被頻繁修改(例如動畫過程中),此時開啓光柵化反而影響效率
3.離屏渲染緩存內容有時間限制,如果100ms內沒有被使用,那麼就會丟棄,無法進行復用
4.離屏渲染的緩存空間有限,是屏幕的2.5倍,超過2.5倍屏幕像素大小的話也會失效,無法實現複用

Instruments 監測離屏渲染
1)Color Offscreen-Rendered Yellow,開啓後會把那些需要離屏渲染的圖層高亮成黃色,這就意味着黃色圖層可能存在性能問題。
2)Color Hits Green and Misses Red,如果 shouldRasterize 被設置成YES,對應的渲染結果會被緩存,如果圖層是綠色,就表示這些緩存被複用;如果是紅色就表示緩存會被重複創建,這就表示該處存在性能問題了。

平衡CPU與GPU
GPU部分:GPU擅長圖形處理,這依賴與GPU內部有成千上萬的計算單位可以並行運算
CPU部分:
而對於文字(CoreText使用CoreGraphics渲染)和圖片(ImageIO)渲染,由於GPU並不擅長做這些工作,不得不先由CPU來處理好以後,再把結果作爲texture傳給GPU
可以使用CoreGraphics給圖片加上圓角,就不需要再另外給圖片容器設置cornerRadius了,可以在CPU空閒的時候進行操作
注意:
1.渲染不是CPU的強項,調用CoreGraphics會消耗其相當一部分計算時間,一般來說CPU渲染都在後臺線程完成(這也是AsyncDisplayKit的主要思想),然後再回到主線程上,把渲染結果傳回CoreAnimation。
2.CPU只適合渲染靜態的元素,如文字、圖片
3.作爲渲染結果的bitmap數據量較大(形式上一般爲解碼後的UIImage),消耗內存較多,所以應該在使用完及時釋放
4.如果使用CPU來做渲染,就沒有理由再觸發GPU的離屏渲染了


參考文章:
https://zhuanlan.zhihu.com/p/381766140
https://juejin.cn/post/6950920557445513229
https://www.cnblogs.com/mysweetAngleBaby/p/16341632.html
https://tbfungeek.github.io/2019/08/04/iOS-渲染系統工作原理介紹/
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章