圖像顯現原理
CPU和GPU兩個硬件是通過總線連接起來的,在CPU輸出的結果是一個位圖,經由總線,在合適的時機,上傳給GPU,GPU拿到位圖后會做相應位圖的圖層渲染,包括紋理合成,之后把結果放到幀緩沖區Frame Buffer中,由視頻控制器根據VSync信號,在指定時間之前,去提取幀緩沖區中屏幕顯示內容,最終顯示到顯示器上
UIView的顯示是CALayer負責,CALayer有個contents屬性,就是最終要繪制到屏幕上的位圖,假如繪制的是label,內容是hello world,那么contents里放置的就是hello world文字的位圖,系統會在合適的時機回調給我們drawRect方法,我們可以在屏幕顯示的基礎之上,去繪制一些自定義想要繪制的內容,繪制好的位圖,最終會經由Core Animation這個框架,提交給GPU部分的OpenGL渲染管線,進行位圖的最終渲染,包括紋理合成,然后顯示到屏幕上面
CPU工作
CPU需要完成UI的布局,包括顯示繪制,之后做一些準備工作,然后把相應的位圖提交到GPU上面
UI布局和文本計算包括控件Frame的設置,對于控件的文字或者size的計算
Display就是繪制過程,drawRect方法發生在這一步驟
PrePare準備階段:假如有UIImageView,那么設置它的image的時候,圖片是不能直接顯示到屏幕上去的,需要對圖片進行解碼
Commit:對最終的輸出結果位圖進行提交
GPU渲染管線過程
指的是OpenGL的渲染管線
首先頂點著色,指的是對位圖進行處理
做完上面五步之后,會把最終的像素點提交到對應的幀緩沖區中,由視頻控制器根據Vsync信號,在指定時間之前,去提取幀緩沖區中屏幕顯示內容,最終顯示到顯示器上
UI卡頓,掉幀的原因
上面是VSync垂直信號
我們一般說的頁面滑動的流暢性是60fps,指的是每一秒會有60幀的畫面更新,那么人眼看到的就是流暢的效果,基于此,相當于每隔16.7毫秒(60分之1)就要產生一幀畫面,在16.7ms之內,需要CPU和GPU共同協同完成最終的一幀的數據,
灰方塊表示CPU花費一定的時間進行文本的布局,UI計算,視圖的繪制,圖片解碼等
然后把位圖提交給GPU,GPU進行圖層的合成,紋理渲染,準備好要顯示的畫面,那么在下一次VSync信號到來時,就可以顯示準備好的這一幀畫面了
假如CPU做工作時時長特別長的時候,留給GPU的時間就比較少了,等GPU將工作做完,總時長就超出16.7了,在下一幀VSync信號到來的時候,沒有準備好當下這一幀的畫面,就產生了掉幀,那么我們肉眼看到的效果就是滑動的卡頓
所以,掉幀的原因是:在規定的16.7ms內,在下一幀VSync信號到來之前,CPU和GPU并沒有完成下一幀畫面的合成,于是導致了卡頓掉幀
基于這個掉幀原因,我們可以分析如何提高Tableview等控件的滑動流暢性優化的方案
滑動優化方案
CPU:文本的布局,UI計算,視圖的繪制,圖片解碼等
減輕CPU的工作時長和壓力
在子線程中進行對象的創建,調整和銷毀
在子線程中預排版(布局計算,文本計算)
讓主線程有更多的時間去響應用戶的交互
預渲染(文本等異步繪制,圖片編解碼等)
GPU:(圖層的合成,紋理渲染等)
紋理渲染方面的優化:
假如我們觸發了離屏渲染,就會產生一些layer的圓角,masksToBoundsdang的設置,陰影蒙層,這些都會觸發GPU層面的離屏渲染,這種情況下,GPU做離屏渲染的工作量就會很大.優化就是盡量避免離屏渲染,還可以依托CPU的異步繪制機制來減輕GPU的壓力
視圖混合方面的優化:
若視圖層級復雜,GPU就要做每一個視圖的合成,合成每個像素點對應的像素值,需要做大量的計算,可以減輕視圖層級復雜性,就能減輕GPU的壓力,也可以通過CPU層面的異步繪制機制,來達到提交的位圖本身就是一個層級少的視圖,這樣也可以減輕GPU的壓力
離屏渲染-GPU層面
當前屏幕渲染:
GPU的渲染操作是在當前用于顯示的屏幕緩沖區中進行
離屏渲染:
GPU在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作
也就是說:當我們設置UI視圖的某些圖層屬性,標記為它在未預合成之前,不能用于當前屏幕上面直接顯示的時候,就會觸發離屏渲染
什么場景下回觸發離屏渲染
- 設置視圖的圓角屬性(必須同時maskToBounds為YES才會觸發)
- 設置視圖的圖層蒙版
- 設置陰影
- 設置光柵化
為何要避免離屏渲染
- 離屏渲染是發生在GPU層面上的,觸發了OpenGL的多通道渲染管線,產生了額外的開銷,所以我們要避免離屏渲染
- 離屏渲染會創建新的渲染緩沖區,會有內存上的開銷,包括對上下文的切換(因為有多通道的渲染管線,所以會需要把多通道的渲染結果做渲染合成,就涉及到了上下文切換),就會有GPU的額外開銷
- 觸發離屏渲染時,會增加GPU的工作量,很可能導致CPU和GPU的總耗時加起來超過了16.7ms,就會導致UI的卡頓和掉幀,所以需要避免離屏渲染
UIView的繪制原理
當我們調用UIView的setNeedsDisplay后,并沒有立刻發生當前視圖的繪制工作,而是在之后的某一時機才會進行當前視圖的繪制
當調用UIView的setNeedsDisplay后,系統會立刻調用view的layer的同名方法,之后相當于在layer上面打上了一個臟標記,然后再當前runloop將要結束的時候,才會調用CALayer的display方法,然后才進入到當前視圖的真正繪制工作流程中
CALayer的display方法,在內部會首先判斷layer的delegate是否響應displayLayer這個方法,若不響應,則系統開始繪制流程
若響應,則開始異步繪制
系統繪制流程
首先CALayer內部會創建一個CGContextRef,在drawRect方法中,可以通過堆棧中的上下文取出這個context,然后layer會判斷它是否有代理,若沒有,則調用CALayer的drawInContext,若有則調用代理方法,做當前視圖的繪制工作,再在合適的時機,基于drawRect回調方法,
drawRect默認操作是什么都不做,而之所以有這個接口,就是為了讓我們在系統繪制之上,可以做些自定義的繪制工作
最后再由CALayer上傳對應的context給GPU,
異步繪制流程
基于layer的delegate,如果實現了displayLayer方法,就可以進入到異步繪制流程當中
在異步繪制過程中
- 需要代理去負責生成對應的bitmap
-
同時需要把bitmap作為layer的contents屬性
假如在某一時機調用了setNeedsDiaplay方法后,在當前runloop將要結束的時候,會有系統調用視圖所對應layer的display方法,如果代理實現了displayLayer方法,會調用這個方法,然后通過子線程的切換,我們會在子線程中去做位圖的繪制,此時主線程可以去做些其他的工作
子線程的繪制
1.通過CGBitmapContextCreat方法,來創建一個位圖的上下文
2.通過CoreGraphic的相關API,可以做當前UI控件的一些繪制工作
3.之后通過CGBitmapContextCreatImage方法,根據所繪制的上下文,生成一張CGImage圖片,然后再回到主隊列中,提交這個位圖,設置給CALayer的contents屬性