Offscreen Rendering
如何檢測(cè)你的項(xiàng)目中是否 觸發(fā)了離屏渲染問(wèn)題
?
那么為何有一些會(huì)觸發(fā)離屏渲染,而有一些卻不會(huì)觸發(fā)呢?下面我們開(kāi)始深入的探索。
離屏渲染的具體過(guò)程
我們知道通常的渲染流程是這樣的:
App通過(guò)CPU和GPU的合作,不停的將內(nèi)容渲染完成放入FrameBuffer幀緩存區(qū),而屏幕顯示不斷從FrameBuffer中獲取內(nèi)容,顯示實(shí)時(shí)的內(nèi)容。
但是離屏渲染的流程是這樣的:
在普通的情況下,GPU直接將渲染好的內(nèi)容放入FrameBuffer中,但是在離屏渲染時(shí)不同,需要先額外創(chuàng)建離屏渲染緩存區(qū)OffscreenBuffer。將提前渲染好的內(nèi)容放入其中,等到合適的時(shí)機(jī)再將OffSreeBuffer中的內(nèi)容進(jìn)一步疊加、渲染。完成后將結(jié)果切換到FrameBuffer中。
離屏渲染的效率問(wèn)題
從上面的流程來(lái)看,離屏渲染時(shí),由于App需要提前對(duì)部分內(nèi)容進(jìn)行額外的渲染并保存到OffScreenBuffer,以及需要在必要時(shí)對(duì)OffScreenBuffer和FrameBuffer進(jìn)行內(nèi)容切換,所以會(huì)需要更長(zhǎng)的處理時(shí)間。(實(shí)際上這兩步切換的代價(jià)是非常大的)。
OffScreenBuffer本身就需要額外的空間,大量的離屏渲染可能造成內(nèi)存過(guò)大的壓力。與此同時(shí),OffScreenBuffer的總大小也是有限的:不能超過(guò)屏幕總像素的2.5倍。
可見(jiàn)離屏渲染的開(kāi)銷(xiāo)非常大,一旦需要離屏渲染的內(nèi)容過(guò)多,就容易造成掉幀問(wèn)題,所以大部分情況下,我們要避免出現(xiàn)離屏渲染。
為什么要用離屏渲染
既然離屏渲染會(huì)造成性能問(wèn)題,那么為什么還要使用離屏渲染?
其實(shí)主要是以下兩種原因:
一些特殊的效果需要使用額外的OffScreenBuffer來(lái)保存渲染中間的狀態(tài),所以不得不使用離屏渲染
出于效率的目的,可以將內(nèi)容提前渲染并保存到OffScreenBuffer中,從而達(dá)到復(fù)用的目的
例如,第一種原因,也就是不得不使用離屏渲染的情況。一般都是系統(tǒng)自動(dòng)觸發(fā)。如:陰影、圓角等。比如我們使用的蒙版(mask)功能,因?yàn)樽罱K的結(jié)果是有超過(guò)一層的渲染結(jié)果進(jìn)行疊加,所以必須要利用額外的內(nèi)存空間對(duì)中間的渲染結(jié)果進(jìn)行保存,因此系統(tǒng)會(huì)默認(rèn)觸發(fā)離屏渲染.
比如,iOS8開(kāi)始提供模糊特效UIBlurEffectView:
先渲染需要模糊的內(nèi)容本身;
對(duì)內(nèi)容進(jìn)行縮放;
對(duì)上一步結(jié)果進(jìn)行垂直模糊;
對(duì)上一步結(jié)果進(jìn)行橫向模糊;
最后一步,將模糊后的結(jié)果進(jìn)行疊加合成,實(shí)現(xiàn)最終完整的模糊效果。
在這樣的5次過(guò)程,系統(tǒng)也會(huì)自動(dòng)觸發(fā)離屏渲染,用來(lái)保存復(fù)雜的特效下,利用額外的內(nèi)存空間對(duì)中間的結(jié)果進(jìn)行保存,以便最后進(jìn)行效果的合成。
離屏渲染的第二種原因:shouldRasterize 光柵化
開(kāi)啟光柵化后,就會(huì)主動(dòng)觸發(fā)離屏渲染。Render Server會(huì)強(qiáng)制將CALayer渲染位圖結(jié)果bitmap保存下來(lái),這樣下次渲染可以直接復(fù)用,提高效率;而保存下來(lái)的bitmap就已經(jīng)包含了layer和sublayer、圓角、陰影、透明度等。
如果layer的構(gòu)成包含了以上幾種元素,結(jié)構(gòu)非常復(fù)雜且還需要重復(fù)利用,可以考慮開(kāi)啟光柵化;因?yàn)閘ayer 上的圓角、陰影、透明度等會(huì)由系統(tǒng)自動(dòng)觸發(fā)離屏渲染,那么打開(kāi)光柵化就可以節(jié)約第二次以及以后的渲染時(shí)間。
而多層的subLayer的情況由于不會(huì)自動(dòng)觸發(fā)離屏渲染,所以相比之下會(huì)花費(fèi)第一次離屏渲染的時(shí)間,但是可以節(jié)約后續(xù)重復(fù)的渲染開(kāi)銷(xiāo)。
使用光柵化的注意點(diǎn):
如果layer并不能被復(fù)用,則沒(méi)必要開(kāi)啟;
如果layer不是靜態(tài)的,需要 被頻繁修改,比如處于動(dòng)畫(huà)之中,那么開(kāi)啟離屏渲染反而影響效率了;
離屏渲染緩存內(nèi)容有時(shí)間限制,緩存內(nèi)容如果100ms沒(méi)被復(fù)用,那么就會(huì)被丟棄,無(wú)法進(jìn)行復(fù)用;
離屏渲染緩存空間有限,超過(guò)2.5倍屏幕像素大小的話,也會(huì)失效,且無(wú)法復(fù)用
圓角的離屏渲染探索
通常來(lái)講,設(shè)置了Layer的圓角效果后,會(huì)自動(dòng)觸發(fā)離屏渲染,但是具體什么情況下設(shè)置圓角才會(huì)觸發(fā)離屏渲染?
如上圖所示,Layer由3層組成,我們?cè)O(shè)置圓角通常是用下面的代碼:
view.layer.cornerRadius?=?2;
CornerRadius - Apple 官方介紹
根據(jù)蘋(píng)果的描述,上面這句代碼,只會(huì)默認(rèn)設(shè)置backgroundColor和border的圓角,而不會(huì)設(shè)置content的圓角,除非設(shè)置了layer.maskToBounds為true(對(duì)應(yīng)view的clickToBounds屬性)。
如果只設(shè)置了CornerRadius而沒(méi)有設(shè)置masksToBounds,由于不需要疊加裁剪,此時(shí)是不會(huì)觸發(fā)離屏渲染的。而當(dāng)設(shè)置了裁剪屬性時(shí),由于maskToBounds會(huì)對(duì)layer以及所有 的subLayer的content都進(jìn)行裁剪,所以不得不觸發(fā)離屏渲染。
view.layer.masksToBounds?=?true?//觸發(fā)離屏渲染的原因
離屏渲染的邏輯
剛才說(shuō)圓角加上masksToBounds時(shí),因?yàn)閙asksToBounds會(huì)對(duì)layer上的所有內(nèi)容進(jìn)行裁剪,從而誘發(fā)了離屏渲染,那么這個(gè)過(guò)程具體是怎么回事呢?我們來(lái)仔細(xì)研究一下:
在普通的layer繪制中,上層的subLayer會(huì)覆蓋下層的subLayer,下層的subLayer在繪制完成后就可以拋棄了,從而節(jié)約空間提高效率。
所有subLayer一次繪制完畢后,整個(gè)繪制過(guò)程完成,就可以進(jìn)行后續(xù)的呈現(xiàn)了。假設(shè)我們需要繪制一個(gè)三層的subLayer,并不設(shè)置裁剪和圓角,那么整個(gè)繪制過(guò)程就如下圖所示:
繪制完進(jìn)行display
設(shè)置了CornerRadius以及masksToBounds
當(dāng)我們?cè)O(shè)置了CornerRadius以及masksToBounds進(jìn)行圓角加裁剪時(shí),masksToBounds裁剪屬性會(huì)應(yīng)用到所有的subLayer上,也就意味著所有的subLayer都要進(jìn)行圓角+裁剪,意味著所有的subLayer在第一次繪制后,并不能立刻丟棄,而必須保存在OffScreenBuffer中等待下一輪的圓角加裁剪操作,這樣便引發(fā)了離屏渲染。
實(shí)際上,并不單只有圓角加裁剪會(huì)觸發(fā)離屏渲染。如果設(shè)置了透明度和組透明(layer.allowsGroupOpacity+layer.opacity),陰影屬性(shadowOffset)等,都會(huì)產(chǎn)生這樣的離屏渲染,因?yàn)檫@些都不是對(duì)單一的layer進(jìn)行處理,而是對(duì)layer及其所有 的subLayer進(jìn)行處理,從而引發(fā)離屏渲染。
避免圓角離屏渲染的手段:
除了盡量減少圓角裁剪的使用,還有什么別的辦法可以避免圓角+裁剪引起的離屏渲染?
由于剛才提到,圓角引起離屏渲染的本質(zhì)是裁剪的疊加,導(dǎo)致了masksToBounds對(duì)layer及其所有的subLayer進(jìn)行了二次處理,那么我們只要避免使用masksToBounds進(jìn)行二次處理,而是對(duì)所有的subLayer進(jìn)行預(yù)處理,就可以只進(jìn)行“畫(huà)家算法(先繪制離屏幕較遠(yuǎn)的圖層,然后繪制距離屏幕較近的圖層,根據(jù)深度值,確定繪制順序)”,用一次疊加就完成繪制。
有哪些可行方案
1.換資源
直接使用帶圓角的圖片,或者替換背景色為帶圓角的純色背景圖,從而避免使用圓角裁剪。不過(guò)這周方法需要依賴(lài)具體情況,并不通用 。
2.mask
再增加一個(gè)和背景色相同的遮罩mask覆蓋在最上層,蓋住四個(gè)角,營(yíng)造出圓角的形狀。但這種方式難以解決背景色為圖片 或漸變色的情況。注意這里的mask并不是指的layer上的mask,而是用兩個(gè)view的疊加
3.UIBezierPath
用貝塞爾曲線繪制閉合帶圓角的矩形,在上下文設(shè)置只有內(nèi)部可見(jiàn),再將不帶圓角的layer渲染成圖片,添加到貝塞爾矩形中。這種方法效率較高,但是layer的布局一旦改變,貝塞爾曲線都需要手動(dòng)進(jìn)行重新繪制,所以需要對(duì)frame、color等進(jìn)行手動(dòng)監(jiān)聽(tīng)并重繪。
4.CoreGraphics
重寫(xiě) drawRect:,用coreGraphics相關(guān)方法,在需要應(yīng)用圓角時(shí)進(jìn)行手動(dòng)繪制。不過(guò)CoreGraphics效率也有限,如果多次調(diào)用也會(huì)有效率問(wèn)題。
觸發(fā)離屏渲染的幾種情況
使用了mask的layer(layer.mask)
需要進(jìn)行裁剪的layer(layer.masksToBounds / view.clipsToBounds)
設(shè)置了組透明度YES,并且透明度不為1的layer (layer.allowsGroupOpacity/layer.opacity)
添加了投影的layer(layer.shadow)
采用了光柵化的layer(layer.shouldRasterize)
繪制了文字的layer (UILabel,CATextLayer,CoreText等)
舉幾個(gè)例子,加深下理解:
//按鈕存在背景圖片??因?yàn)?會(huì)對(duì)button的layer和其imageView的layer進(jìn)行圓角加裁剪?所以會(huì)觸發(fā)離屏渲染 UIButton?*btn1?=?[UIButton?buttonWithType:UIButtonTypeCustom]; btn1.frame?=?CGRectMake(100,?30,?100,?100); [btn1?setImage:[UIImage?imageNamed:@"btn.png"]?forState:UIControlStateNormal]; [self.view?addSubview:btn1]; btn1.layer.cornerRadius?=?50; btn1.clipsToBounds?=?YES;
//按鈕存在背景圖片??只設(shè)置了clipsToBounds,裁剪是針對(duì)于imageview的,所以不會(huì)觸發(fā)離屏渲染(not?offscreen?rendering) UIButton?*btn1?=?[UIButton?buttonWithType:UIButtonTypeCustom]; btn1.frame?=?CGRectMake(100,?30,?100,?100); [btn1?setImage:[UIImage?imageNamed:@"btn.png"]?forState:UIControlStateNormal]; [self.view?addSubview:btn1]; btn1.imageView.layer.cornerRadius?=?50; btn1.clipsToBounds?=?YES;
//按鈕不存在背景圖片?雖然設(shè)置了圓角+裁剪,但是只有一層layer??所以不會(huì)觸發(fā)離屏渲染 UIButton?*btn2?=?[UIButton?buttonWithType:UIButtonTypeCustom]; btn2.frame?=?CGRectMake(100,?180,?100,?100); btn2.backgroundColor?=?[UIColor?blueColor]; [self.view?addSubview:btn2]; btn2.layer.cornerRadius?=?50; btn2.clipsToBounds?=?YES;
//UIImageView?設(shè)置了圖片+背景色;?背景色和圖片兩層layer都需要進(jìn)行圓角+裁剪??所以會(huì)觸發(fā)離屏渲染 UIImageView?*img1?=?[[UIImageView?alloc]init]; img1.frame?=?CGRectMake(100,?320,?100,?100); img1.backgroundColor?=?[UIColor?blueColor]; img1.image?=?[UIImage?imageNamed:@"btn.png"]; [self.view?addSubview:img1]; img1.layer.cornerRadius?=?50; img1.layer.masksToBounds?=?YES;
//UIImageView?只設(shè)置了圖片,無(wú)背景色;?只有一層?layer?需要圓角+裁剪?所以不會(huì)觸發(fā)離屏渲染 UIImageView?*img2?=?[[UIImageView?alloc]init]; img2.frame?=?CGRectMake(100,?480,?100,?100); img2.image?=?[UIImage?imageNamed:@"btn.png"]; [self.view?addSubview:img2]; img2.layer.cornerRadius?=?50; img2.layer.masksToBounds?=?YES;
編輯:黃飛
?
評(píng)論
查看更多