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-渲染系統工作原理介紹/