我們在平時(shí)的iOS面試中,經(jīng)常會(huì)遇到有關(guān)離屏渲染(Offscreen rendering
)的知識(shí)點(diǎn)愿待。一般來說蝇更,絕大多數(shù)人都能答出圓角、mask呼盆、陰影會(huì)觸發(fā)離屏渲染
,但是也僅止于此蚁廓。如果再問得深入哪怕一點(diǎn)點(diǎn)访圃,比如:
離屏渲染是在哪一步進(jìn)行的?為什么相嵌?
設(shè)置cornerRadius
一定會(huì)觸發(fā)離屏渲染嗎腿时?
90%的候選人都沒法非常確定地說出答案。作為一個(gè)客戶端工程師饭宾,把控渲染性能是最關(guān)鍵批糟、最獨(dú)到的技術(shù)要點(diǎn)之一,如果僅僅了解表面知識(shí)看铆,到了實(shí)際應(yīng)用時(shí)往往會(huì)失之毫厘謬以千里徽鼎,無法得到預(yù)期的效果。
離屏渲染的定義
如果要在顯示屏上顯示內(nèi)容,我們至少需要一塊與屏幕像素?cái)?shù)據(jù)量一樣大的frame buffer
作為像素?cái)?shù)據(jù)存儲(chǔ)區(qū)域否淤,而這也是GPU存儲(chǔ)渲染結(jié)果的地方悄但。如果有時(shí)因?yàn)槊媾R一些限制,無法把渲染結(jié)果直接寫入frame buffer石抡,而是先暫存在另外的內(nèi)存區(qū)域
檐嚣,之后再寫入frame buffer
,那么這個(gè)過程被稱之為離屏渲染
啰扛。
CPU”離屏渲染“
這里所說的CPU的離屏渲染并不是真正的離屏渲染,大家知道隐解,如果我們在UIView
中實(shí)現(xiàn)了drawRect
方法鞍帝,就算它的函數(shù)體內(nèi)部實(shí)際沒有代碼,系統(tǒng)也會(huì)為這個(gè)view
申請一塊內(nèi)存區(qū)域厢漩,等待CoreGraphics
可能的繪畫操作膜眠。
對于類似這種新開一塊CGContext來畫圖
的操作,有很多文章和視頻也稱之為"離屏渲染"
(因?yàn)橄袼財(cái)?shù)據(jù)是暫時(shí)存入了CGContext溜嗜,而不是直接到了frame buffer)宵膨。進(jìn)一步來說,其實(shí)所有CPU進(jìn)行的光柵化操作(如文字渲染炸宵、圖片解碼)辟躏,都無法直接繪制到由GPU掌管的frame buffer,只能暫時(shí)先放在另一塊內(nèi)存之中土全,說起來都屬于“離屏渲染”捎琐。
自然我們會(huì)認(rèn)為,因?yàn)镃PU不擅長做這件事裹匙,所以我們需要盡量避免它瑞凑,就誤以為這就是需要避免離屏渲染的原因。但是CPU渲染并非真正意義上的離屏渲染概页。證據(jù)就是籽御,如果你的view實(shí)現(xiàn)了drawRect,此時(shí)打開Xcode調(diào)試的“Color offscreen rendered yellow”
開關(guān)惰匙,你會(huì)發(fā)現(xiàn)這片區(qū)域不會(huì)被標(biāo)記為黃色技掏,說明Xcode并不認(rèn)為這屬于離屏渲染。
其實(shí)通過CPU渲染就是俗稱的“軟件渲染”
项鬼,而真正的離屏渲染發(fā)生在GPU哑梳。
GPU離屏渲染
在上面的渲染流水線示意圖中我們可以看到,主要的渲染操作都是由CoreAnimation
的Render Server
模塊绘盟,通過調(diào)用顯卡驅(qū)動(dòng)所提供的OpenGL/Metal
接口來執(zhí)行的鸠真。通常對于每一層layer悯仙,Render Server會(huì)遵循畫家算法
,按次序輸出到frame buffer
弧哎,后一層覆蓋前一層雁比,就能得到最終的顯示結(jié)果(值得一提的是,與一般桌面架構(gòu)不同撤嫩,在iOS中偎捎,設(shè)備主存和GPU的顯存共享物理內(nèi)存
,這樣可以省去一些數(shù)據(jù)傳輸開銷)序攘。
然而有些場景并沒有那么簡單。作為“畫家”的GPU雖然可以一層一層往畫布上進(jìn)行輸出程奠,但是無法在某一層渲染完成之后丈牢,再回過頭來擦除/改變其中的某個(gè)部分——因?yàn)樵谶@一層之前的若干層layer像素?cái)?shù)據(jù),已經(jīng)在渲染中被永久覆蓋了瞄沙。這就意味著距境,對于每一層layer,要么能找到一種通過單次遍歷就能完成渲染的算法,要么就不得不另開一塊內(nèi)存,借助這個(gè)臨時(shí)中轉(zhuǎn)區(qū)域來完成一些更復(fù)雜的忆蚀、多次的修改/剪裁操作男旗。
什么時(shí)候會(huì)觸發(fā)離屏渲染
如果需要使用離屏緩存區(qū)(Offscreen Buffer)
進(jìn)行位圖的處理時(shí)泽台,就會(huì)觸發(fā)離屏渲染。常見的情況有下面幾種:
-
cornerRadius+clipsToBounds/layer.masksToBounds
,這里我們來看一段代碼
//1.按鈕存在背景圖片
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 30, 100, 100);
btn1.layer.cornerRadius = 50;
[self.view addSubview:btn1];
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
btn1.clipsToBounds = YES;
//2.按鈕不存在背景圖片
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, 180, 100, 100);
btn2.layer.cornerRadius = 50;
btn2.backgroundColor = [UIColor blueColor];
[self.view addSubview:btn2];
btn2.clipsToBounds = YES;
//3.UIImageView 設(shè)置了圖片+背景色;
UIImageView *img1 = [[UIImageView alloc]init];
img1.frame = CGRectMake(100, 320, 100, 100);
img1.backgroundColor = [UIColor blueColor];
[self.view addSubview:img1];
img1.layer.cornerRadius = 50;
img1.layer.masksToBounds = YES;
img1.image = [UIImage imageNamed:@"btn.png"];
//4.UIImageView 只設(shè)置了圖片,無背景色;
UIImageView *img2 = [[UIImageView alloc]init];
img2.frame = CGRectMake(100, 480, 100, 100);
[self.view addSubview:img2];
img2.layer.cornerRadius = 50;
img2.layer.masksToBounds = YES;
img2.image = [UIImage imageNamed:@"btn.png"];
模擬器運(yùn)行檐束,通過Debug-Color offscreen rendered
,可以看到只有1和3
觸發(fā)了離屏渲染绪妹,而2和4
并沒有觸發(fā)。
這是為什么呢?
想解釋這個(gè)我們需要先了解一下layer
的結(jié)構(gòu)
layer
是由backgroundColor、contents、borderWidth&borderColor
構(gòu)成的离唐,跟我們即將解釋的圓角觸發(fā)離屏渲染息息相關(guān)域庇,我們在蘋果的官方文檔可以找到答案
官方文檔告訴我們庵朝,設(shè)置
cornerRadius
只會(huì)對CALayer中的backgroundColor 和 border
設(shè)置圓角偿短,不會(huì)設(shè)置contents
的圓角欣孤,如果contents需要設(shè)置圓角,需要同時(shí)將maskToBounds / clipsToBounds
設(shè)置為true昔逗。
發(fā)現(xiàn)規(guī)律:
- 當(dāng)只設(shè)置
backgroundColor降传、border
,而contents中沒有子視圖時(shí)勾怒,無論maskToBounds / clipsToBounds
是true
還是false
婆排,都不會(huì)觸發(fā)離屏渲染- 當(dāng)contents中有子視圖時(shí),此時(shí)設(shè)置
cornerRadius
+maskToBounds / clipsToBounds
,就會(huì)觸發(fā)離屏渲染笔链,但是這種情況在UIImageView中并不適用段只,當(dāng)UIImageView中只設(shè)置圖片+maskToBounds / clipsToBounds
是不會(huì)觸發(fā)離屏渲染,蘋果對UIImageView優(yōu)化我想也只是將image直接畫在了contents上面這樣不設(shè)置背景色其實(shí)只需要渲染一個(gè)layer,所以不需要用到離屏緩沖區(qū)鉴扫,
赞枕,所以不會(huì)產(chǎn)生離屏渲染,如果此時(shí)再加上背景色坪创,就會(huì)觸發(fā)離屏渲染炕婶。
shadow,其原因在于莱预,雖然layer本身是一塊矩形區(qū)域柠掂,但是陰影默認(rèn)是作用在其中”非透明區(qū)域“的,而且需要顯示在所有l(wèi)ayer內(nèi)容的下方依沮,因此根據(jù)畫家算法必須被渲染在先涯贞。但矛盾在于此時(shí)陰影的本體(layer和其子layer)都還沒有被組合到一起,怎么可能在第一步就畫出只有完成最后一步之后才能知道的形狀呢危喉?這樣一來又只能另外申請一塊內(nèi)存宋渔,把本體內(nèi)容都先畫好,再根據(jù)渲染結(jié)果的形狀辜限,添加陰影到frame buffer傻谁,最后把內(nèi)容畫上去(這只是我的猜測,實(shí)際情況可能更復(fù)雜)列粪。不過如果我們能夠預(yù)先告訴
CoreAnimation
(通過shadowPath
屬性)陰影的幾何形狀审磁,那么陰影當(dāng)然可以先被獨(dú)立渲染出來,不需要依賴layer本體岂座,也就不再需要離屏渲染了态蒂。group opacity
(組透明度),其實(shí)從名字就可以猜到费什,alpha并不是分別應(yīng)用在每一層之上钾恢,而是只有到整個(gè)layer樹畫完之后,再統(tǒng)一加上alpha鸳址,最后和底下其他layer的像素進(jìn)行組合瘩蚪。顯然也無法通過一次遍歷就得到最終結(jié)果。將一對藍(lán)色和紅色layer疊在一起稿黍,然后在父layer上設(shè)置opacity=0.5疹瘦,并復(fù)制一份在旁邊作對比。左邊關(guān)閉group opacity巡球,右邊保持默認(rèn)(從iOS7開始言沐,如果沒有顯式指定,group opacity會(huì)默認(rèn)打開)酣栈,然后打開offscreen rendering
的調(diào)試险胰,我們會(huì)發(fā)現(xiàn)右邊的那一組確實(shí)是離屏渲染了。
- mask矿筝,我們知道m(xù)ask是應(yīng)用在layer和其所有子layer的組合之上的起便,而且可能帶有透明度,那么其實(shí)和group opacity的原理類似窖维,不得不在離屏渲染中完成榆综。
- UIBlurEffect,同樣無法通過一次遍歷完成
GPU離屏渲染的性能影響
GPU的操作是高度流水線化的陈辱。本來所有計(jì)算工作都在有條不紊地正在向frame buffer
輸出奖年,此時(shí)突然收到指令,需要輸出到另一塊內(nèi)存沛贪,那么流水線中正在進(jìn)行的一切都不得不被丟棄陋守,切換到只能服務(wù)于我們當(dāng)前的操作(例如“切圓角”)。等到完成以后再次清空利赋,再回到向frame buffer
輸出的正常流程水评。
在tableView
或者collectionView
中,滾動(dòng)的每一幀變化都會(huì)觸發(fā)每個(gè)cell的重新繪制媚送,因此一旦存在離屏渲染中燥,上面提到的上下文切換就會(huì)每秒發(fā)生60次
,并且很可能每一幀有幾十張的圖片要求這么做塘偎,對于GPU的性能沖擊可想而知(GPU非常擅長大規(guī)模并行計(jì)算疗涉,但是我想頻繁的上下文切換顯然不在其設(shè)計(jì)考量之中)
善用離屏渲染
盡管離屏渲染開銷很大拿霉,但是當(dāng)我們無法避免它的時(shí)候,可以想辦法把性能影響降到最低咱扣。優(yōu)化思路也很簡單:既然已經(jīng)花了不少精力把圖片裁出了圓角绽淘,如果我能把結(jié)果緩存下來,那么下一幀渲染就可以復(fù)用這個(gè)成果闹伪,不需要再重新畫一遍了沪铭。
CALayer為這個(gè)方案提供了對應(yīng)的解法:shouldRasterize
(光柵化)。一旦被設(shè)置為true偏瓤,Render Server
就會(huì)強(qiáng)制把layer的渲染結(jié)果(包括其子layer杀怠,以及圓角、陰影厅克、group opacity等等)保存在一塊內(nèi)存中赔退,這樣一來在下一幀仍然可以被復(fù)用,而不會(huì)再次觸發(fā)離屏渲染已骇。有幾個(gè)需要注意的點(diǎn):
When the value of this property is YES, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.
當(dāng)我們開啟光柵化時(shí)离钝,會(huì)將layer渲染成位圖保存在緩存中,這樣在下次使用時(shí)褪储,就可以直接復(fù)用卵渴,提高效率。
針對光柵化的使用鲤竹,有以下幾個(gè)建議:
- 如果layer不能被復(fù)用浪读,則沒有必要開啟光柵化
- 如果layer不是靜態(tài),需要被頻繁修改(例如動(dòng)畫過程中)辛藻,此時(shí)開啟光柵化反而影響效率
- 離屏渲染緩存內(nèi)容有時(shí)間限制碘橘,如果100ms內(nèi)沒有被使用,那么就會(huì)丟棄吱肌,無法進(jìn)行復(fù)用
- 離屏渲染的緩存空間有限痘拆,是屏幕的2.5倍,超過2.5倍屏幕像素大小的話也會(huì)失效氮墨,無法實(shí)現(xiàn)復(fù)用