深入研究離屏渲染

我們在平時(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è)過程被稱之為離屏渲染啰扛。

渲染結(jié)果先經(jīng)過了離屏buffer嚎京,再到frame buffer

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離屏渲染

在上面的渲染流水線示意圖中我們可以看到,主要的渲染操作都是由CoreAnimationRender 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結(jié)構(gòu)

layer是由backgroundColor、contents、borderWidth&borderColor構(gòu)成的离唐,跟我們即將解釋的圓角觸發(fā)離屏渲染息息相關(guān)域庇,我們在蘋果的官方文檔可以找到答案

apple文檔

官方文檔告訴我們庵朝,設(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 / clipsToBoundstrue還是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ù)用
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纺蛆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子规揪,更是在濱河造成了極大的恐慌桥氏,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猛铅,死亡現(xiàn)場離奇詭異字支,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門堕伪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來揖庄,“玉大人,你說我怎么就攤上這事欠雌】侔” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵桨昙,是天一觀的道長。 經(jīng)常有香客問我腌歉,道長蛙酪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任翘盖,我火速辦了婚禮桂塞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘馍驯。我一直安慰自己阁危,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布汰瘫。 她就那樣靜靜地躺著狂打,像睡著了一般。 火紅的嫁衣襯著肌膚如雪混弥。 梳的紋絲不亂的頭發(fā)上趴乡,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機(jī)與錄音蝗拿,去河邊找鬼晾捏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛哀托,可吹牛的內(nèi)容都是我干的惦辛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼仓手,長吁一口氣:“原來是場噩夢啊……” “哼胖齐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起俗或,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤市怎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后辛慰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體区匠,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了驰弄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片麻汰。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖戚篙,靈堂內(nèi)的尸體忽然破棺而出五鲫,到底是詐尸還是另有隱情,我是刑警寧澤岔擂,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布位喂,位于F島的核電站,受9級(jí)特大地震影響乱灵,放射性物質(zhì)發(fā)生泄漏塑崖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一痛倚、第九天 我趴在偏房一處隱蔽的房頂上張望规婆。 院中可真熱鬧,春花似錦蝉稳、人聲如沸抒蚜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗡髓。三九已至,卻和暖如春毕莱,著一層夾襖步出監(jiān)牢的瞬間器贩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工朋截, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛹稍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓部服,卻偏偏與公主長得像唆姐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子廓八,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容