CALayer離屏渲染問題

CALayer與UIView

iOS界面中,看到的界面元素基本都是UIView,例如按鈕,文本,圖片等都是集成自UIView。事實(shí)上,UIView并沒有繪制的功能,UIView的繪制工作是CoreAnimation框架完成的,也就是UIView的屬性變量CALayer完成繪制工作的。

//摘自UIView的頭文件聲明
@property(nonatomic,readonly,strong)                 CALayer  *layer;              
// returns view's layer. Will always return a non-nil value. view is layer's delegate

看這個(gè)注釋,可以把UIView當(dāng)作CALayer的delegate,UIView只是來方便管理CALayer而已。同時(shí),可以把CALayer理解為,這是一個(gè)圖層,類似PS的圖層概念,而且一個(gè)UIView可以有多個(gè)圖層。

UIView繼承自UIResponder, 能接收并響應(yīng)事件, 負(fù)責(zé)顯示內(nèi)容的管理。而CALayer繼承自NSObject, 并不能響應(yīng)事件, 負(fù)責(zé)顯示內(nèi)容的繪制。所以更改UIView的frame,bounds或者backgroundColor等這些屬性的時(shí)候,其實(shí)是在更改CALayer的屬性。

UIView與CALayer一樣是有層次的關(guān)系的,每個(gè)UIView和CALayer都可能有子view或者子layer,當(dāng)子view在層級(jí)關(guān)系中添加或者被移除的時(shí)候,他們關(guān)聯(lián)的layer也同樣對(duì)應(yīng)在層級(jí)關(guān)系樹當(dāng)中有相同的添加或者被移除操作。

屏幕顯示

移動(dòng)設(shè)備中顯示系統(tǒng)一般是由CPU繪制好顯示內(nèi)容,GPU渲染結(jié)束后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會(huì)按照VSync信號(hào)(垂直同步)讀取幀緩沖區(qū)的數(shù)據(jù),然后通過數(shù)模轉(zhuǎn)換,發(fā)送給顯示器顯示。
如果當(dāng)一個(gè)VSync信號(hào)來臨的時(shí)候,幀緩沖區(qū)還沒有新的渲染結(jié)果,就會(huì)使用舊的,而當(dāng)前幀就會(huì)被丟棄。這就是為什么會(huì)卡幀的原因,CPU或者GPU繪制渲染跟不上VSync信號(hào)頻率。

iOS系統(tǒng)使用的是雙緩沖區(qū)來緩沖渲染結(jié)果,GPU會(huì)把渲染結(jié)果依次放入兩個(gè)緩沖區(qū),當(dāng)視頻控制器讀取第一個(gè)緩沖區(qū)的時(shí)候,GPU繼續(xù)渲染好第二幀顯示內(nèi)容放入第二個(gè)緩沖區(qū),然后把視頻控制器的指針指向第二個(gè)緩沖區(qū),這樣下一個(gè)VSync信號(hào)來臨的時(shí)候,就直接讀取第二個(gè)緩沖區(qū)的顯示內(nèi)容。

畫面撕裂

由于雙緩沖的設(shè)計(jì),容易導(dǎo)致當(dāng)視頻控制器正在讀取A緩沖區(qū)的內(nèi)容的時(shí)候,讀到一半,顯示器也顯示一半內(nèi)容出來了。此時(shí)GPU又剛好渲染完畢,把渲染結(jié)果存放在B緩沖區(qū),同時(shí)把視頻控制器的指針指到B緩沖區(qū)。那這時(shí)候讀取的后一半就是B緩沖區(qū)的結(jié)果,也就是下一幀的內(nèi)容,這時(shí)候就會(huì)出現(xiàn)畫面撕裂的情況。要解決這個(gè)問題,引入一個(gè)機(jī)制開啟垂直同步,也就是說,GPU渲染完畢,要等待VSync信號(hào)更新后才提交緩沖區(qū)里面。

關(guān)于水平同步信號(hào)HSync和垂直同步VSync的區(qū)別,顯示系統(tǒng)內(nèi)部類似電子槍掃描一樣。一幀畫面需要水平同步信號(hào)HSync來控制電子槍的從左到右掃描。掃描完一行后,根據(jù)水平同步信號(hào)HSync,切換到最左邊,開始下一行掃描。當(dāng)掃描完最后一行,根據(jù)垂直同步VSync,回到第一行的最左邊位置。

image.png

屏幕渲染

在 OpenGL 中,GPU 屏幕渲染有以下兩種方式:

On-Screen Rendering:
即當(dāng)前屏幕渲染,在用于顯示的屏幕緩沖區(qū)中進(jìn)行,不需要額外創(chuàng)建新的緩存,也不需要開啟新的上下文,所以性能較好,但是受到緩存大小限制等因素,一些復(fù)雜的操作無法完成。

Off-Screen Rendering:
即離屏渲染,指的是在 GPU 的當(dāng)前屏幕緩沖區(qū)外開辟新的緩沖區(qū)進(jìn)行操作。簡單來說就是指的是在圖像在繪制到當(dāng)前屏幕前,需要先進(jìn)行一次渲染,之后才繪制到當(dāng)前屏幕。

相比于當(dāng)前屏幕渲染,離屏渲染的代價(jià)是很高的,主要體現(xiàn)在如下兩個(gè)方面:

  • 創(chuàng)建新的緩沖區(qū)
  • 上下文切換
    離屏渲染的整個(gè)過程,需要多次切換上下文環(huán)境:先從當(dāng)前屏幕切換到離屏,等待離屏渲染結(jié)束后,將離屏緩沖區(qū)的渲染結(jié)果顯示到到屏幕上,這又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕。

離屏渲染的定義問題

這里提到的offscreen rendering主要講的是通過GPU執(zhí)行的offscreen,事實(shí)上還有的offscreen rendering是通過CPU來執(zhí)行的(例如使用Core Graphics, drawRect,下面的添加圓角方法中,第二第三中方法也產(chǎn)生了離屏渲染,只不過是CPU渲染的)。

其它類似cornerRadios, masks, shadows等觸發(fā)的offscreen是基于GPU的。許多人有誤區(qū),認(rèn)為offscreen rendering就是software rendering,只是純粹地靠CPU運(yùn)算。實(shí)際上并不是的,offscreen rendering是個(gè)比較復(fù)雜,涉及許多方面的內(nèi)容。

通俗來說就是,顯示系統(tǒng)不能第一時(shí)間顯示內(nèi)容,需要開辟buff區(qū)預(yù)渲染內(nèi)容才能顯示,就是離屏渲染。

當(dāng)對(duì)CALayer設(shè)置以下屬性的時(shí)候,會(huì)導(dǎo)致離屏渲染(基于GPU)問題。
shouldRasterize(光柵化)
masks(遮罩)
shadows(陰影)
edge antialiasing(抗鋸齒)
group opacity(不透明)

注意:shouldRasterize = YES 會(huì)使視圖渲染內(nèi)容被緩存起來,下次繪制的時(shí)候可以直接顯示緩存,如果顯示的內(nèi)容不是動(dòng)態(tài)變化的,這樣使用緩存的方式,效率也挺不錯(cuò)的。

由于UIView并沒有設(shè)置圓角的API,一般設(shè)置View的圓角的時(shí)候可以使用CALayer來設(shè)置,但是這樣容易導(dǎo)致離屏渲染問題,當(dāng)屏幕出現(xiàn)過多圓角的時(shí)候,會(huì)出現(xiàn)嚴(yán)重卡頓,GPU滿載,而CPU空閑的狀態(tài)。

當(dāng)然,你也可以使用CPU渲染,CPU浮點(diǎn)計(jì)算能力不如GPU,但是渲染一些簡單界面還是沒問題的。因?yàn)镃PU渲染不同于GPU渲染,不會(huì)產(chǎn)生離屏渲染問題,自然也就沒有上下文切換的效率問題,有些場(chǎng)景效率可能更高。使用CPU渲染,一般就是重寫 drawRect 方法,然后使用 Core Graphics 的技術(shù)進(jìn)行了繪制操作,這個(gè)渲染過程由 CPU 在 App 內(nèi)同步地完成,渲染得到的bitmap最后再交由GPU用于顯示。

處理圓角

一般處理圓角,比較快捷的方式是

view.layer.cornerRadius = 6.0;
view.layer.masksToBounds = YES;

但是這種方式會(huì)觸發(fā)兩次離屏渲染,對(duì)性能影響較大??梢愿倪M(jìn)一下,如下所示

  • 如果內(nèi)容不是動(dòng)態(tài)改變,可以使用shouldRasterize = YES,緩存成bitmap未嘗不可;
  • 是UIImageView設(shè)置圓角的話,可以使用貝賽爾曲線工具UIBezierPath對(duì)UIImage進(jìn)行裁剪成圓角再顯示。
- (void)drawRect:(CGRect)rect {
  CGRect bounds = self.bounds;
  [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];
  [self.image drawInRect:bounds];
}

這種方法只會(huì)觸發(fā)一次離屏渲染(基于CPU),但是會(huì)到時(shí)內(nèi)存的增加。

  • 利用CoreGraphics繪制出一個(gè)圓角矩形的上下文,然后將圖片畫到上下文中,最后通過上下文獲取裁剪好的圖片。同樣也會(huì)觸發(fā)一次基于CPU的離屏渲染。

  • 直接給當(dāng)前的view加一層圓角遮罩層,比較局限,對(duì)背景有要求。

  • 直接讓UI把圖片裁剪好

Blending 圖像混合

這里順便提一下圖像系統(tǒng)中的圖像混合,與屏幕渲染一樣的是,Blending同樣會(huì)導(dǎo)致性能問題,但是影響沒有離屏渲染這么嚴(yán)重。當(dāng)兩個(gè)圖層疊加在一起,如果第一個(gè)圖層的透明的,則最終像素的顏色計(jì)算需要將第二個(gè)圖層也考慮進(jìn)來。這一過程即為Blending。

會(huì)導(dǎo)致blending的原因:

  • layer(UIView)的Alpha < 1
  • UIImgaeView的image含有Alpha channel(即使UIImageView的alpha是1,但只要image含透明通道,則仍會(huì)導(dǎo)致Blending)

為什么Blending會(huì)導(dǎo)致性能的損失?
原因是很直觀的,如果一個(gè)圖層是不透明的,則系統(tǒng)直接顯示該圖層的顏色即可。而如果圖層是透明的,則會(huì)引入更多的計(jì)算,因?yàn)樾枰严旅娴膱D層也包括進(jìn)來,進(jìn)行混合后顏色的計(jì)算。

參考文章

Advanced Graphics and Animations for iOS Apps

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容