iOS 深入理解離屏渲染

前言

離屏渲染(Offscreen Rendering),對(duì)于這個(gè)概念作為iOS開發(fā)者相信大家并不陌生霜幼,多多少少會(huì)有一些了解恬惯,比如“設(shè)置圓角、mask烤咧、陰影會(huì)觸發(fā) 離屏渲染”偏陪。那么先引出一個(gè)問題?

設(shè)置圓角(cornerRadius)一定會(huì)觸發(fā)離屏渲染嗎髓削?

我們不妨敲敲代碼來測(cè)試一下:
讀者也可以一邊閱讀代碼 一邊猜想下面4組情況 會(huì)不會(huì)觸發(fā)離屏渲染竹挡。

    //1.按鈕1
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(2, 320, 90, 90);
    btn1.backgroundColor = [UIColor redColor];
    btn1.layer.cornerRadius = 45;
    btn1.layer.masksToBounds = YES;
    [self.view addSubview:btn1];
    
    //2.按鈕2
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(96, 320, 90, 90);
    [btn2 setImage:[UIImage imageNamed:@"btn.jpeg"] forState:UIControlStateNormal];
    btn2.layer.cornerRadius = 45;
    btn2.layer.masksToBounds = YES;
    [self.view addSubview:btn2];
    
    
    //3.img1
    UIImageView *img1 = [[UIImageView alloc]init];
    img1.frame = CGRectMake(190, 320, 90, 90);
    img1.image = [UIImage imageNamed:@"btn.jpeg"];
    img1.layer.cornerRadius = 45;
    img1.layer.masksToBounds = YES;
    [self.view addSubview:img1];
    
    //4.img2
    UIImageView *img2 = [[UIImageView alloc]init];
    img2.frame = CGRectMake(284, 320, 90, 90);
    img2.image = [UIImage imageNamed:@"btn.jpeg"];
    img2.backgroundColor = [UIColor blueColor];
    img2.layer.cornerRadius = 45;
    img2.layer.masksToBounds = YES;
    [self.view addSubview:img2];
    

你的答案是什么呢?對(duì)于上面4組案例我們均 設(shè)置了layer.cornerRadius = 45; 那么是否都會(huì)觸發(fā)離屏渲染呢立膛?

我們先來驗(yàn)證一下答案揪罕。

打開Simulator的離屏渲染顏色標(biāo)記,

開啟Color Off-screen Rendered

在模擬器運(yùn)行宝泵,結(jié)果如下

測(cè)試對(duì)比圖

如圖可見好啰,上面4組測(cè)試,我們都設(shè)置了layer.cornerRadius = 45;儿奶。但是框往,第1組和第3組圖并沒有觸發(fā)離屏渲染。至此對(duì)于上面引出的問題闯捎,我們至少可以得出一個(gè)結(jié)論:

設(shè)置圓角(cornerRadius)不一定會(huì)觸發(fā)離屏渲染椰弊!

如果你認(rèn)為 同時(shí) 設(shè)置layer.cornerRadius = 45;layer.masksToBounds = YES; 就會(huì)觸發(fā)许溅。那么第1組和第3組測(cè)試已經(jīng)將這個(gè)想法否定了。

那么到底什么情況下才會(huì)觸發(fā)離屏渲染呢秉版?

離屏渲染觸發(fā)的原因

在這里我們先來簡(jiǎn)單了解一下圖像的渲染流程:

渲染流水線

CPU計(jì)算贤重、解碼——>GPU渲染——>幀緩存區(qū)——>視頻控制器逐行讀取(位圖)——>數(shù)模轉(zhuǎn)化——>顯示清焕。

如果要在顯示屏上顯示內(nèi)容并蝗,我們至少需要一塊與屏幕像素?cái)?shù)據(jù)量一樣大的frame buffer,作為像素?cái)?shù)據(jù)存儲(chǔ)區(qū)域秸妥,而這也是GPU存儲(chǔ)渲染結(jié)果的地方滚停。

通常情況下,CPU對(duì)將要顯示的圖像進(jìn)行計(jì)算粥惧、解碼键畴,然后交給GPU,GPU將渲染好的內(nèi)容存入幀緩存區(qū)突雪,在下一次Runloop到來的時(shí)候镰吵,由視頻控制器逐行掃描幀緩存區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)化挂签,最終交給顯示器顯示。

幀緩存區(qū)

但是盼产,當(dāng)圖形需要做額外的渲染處理的時(shí)候饵婆,(例如對(duì)所有的圖層切圓角處理),此時(shí)不能直接將幀緩存中的數(shù)據(jù)交由視頻控制器讀认肥邸(還未處理完成)侨核,而在正常的渲染流程中,我們無法對(duì)所有的圖層進(jìn)行圓角處理灌灾,因?yàn)殇秩就戤叺囊呀?jīng)被幀緩存丟棄搓译,所以我們就需要額外的一個(gè)新的緩沖區(qū)(離屏緩沖區(qū))來存儲(chǔ)我們不能第一時(shí)間交給視頻控制器讀取的數(shù)據(jù),等待離屏緩沖區(qū)要處理的全部數(shù)據(jù)渲染锋喜、組合完畢些己,再交給幀緩存區(qū),然后依次走下面的流程嘿般。這種在離屏緩沖區(qū)渲染的過程我們稱之為 離屏渲染段标。

離屏緩沖區(qū)

在上面的渲染流水線示意圖中我們可以看到,主要的渲染操作都是由CoreAnimation的Render Server模塊炉奴,通過調(diào)用顯卡驅(qū)動(dòng)所提供的OpenGL/Metal接口來執(zhí)行的逼庞。通常對(duì)于每一層layer,Render Server會(huì)遵循“畫家算法”瞻赶,也就是由遠(yuǎn)到近的方式將圖層繪制到屏幕上赛糟,繪制近距離圖層會(huì)有覆蓋遠(yuǎn)圖層的邏輯派任。繪制完一層,就會(huì)將該層從幀緩存區(qū)中移除(以節(jié)省空間)璧南。

畫家算法

然而有些場(chǎng)景并沒有那么簡(jiǎn)單掌逛。作為“畫家”的GPU雖然可以一層一層往畫布上進(jìn)行輸出,但是無法在某一層渲染完成之后穆咐,再回過頭來擦除/改變其中的某個(gè)部分——因?yàn)樵谶@一層之前的若干層layer像素?cái)?shù)據(jù)颤诀,已經(jīng)在渲染中被永久覆蓋了。這就意味著对湃,對(duì)于每一層layer崖叫,要么能找到一種通過單次遍歷就能完成渲染的算法,要么就不得不另開一塊內(nèi)存拍柒,借助這個(gè)臨時(shí)中轉(zhuǎn)區(qū)域來完成一些更復(fù)雜的心傀、多次的修改/剪裁操作。

了解了 離屏渲染 的概念拆讯,我們?cè)賮砜匆幌绿O果官方對(duì)于cornerRadius的描述脂男。

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to YES causes the content to be clipped to the rounded corners.

The default value of this property is 0.0.

解釋:當(dāng)設(shè)置大于0的cornerRadius值時(shí),角半徑僅僅適用于layer的backgroundColorborder 种呐,并不適用于圖像宰翅,除非同時(shí)設(shè)置了layer.masksToBoundstrue,才會(huì)對(duì)layer的contents也設(shè)置圓角爽室。

結(jié)合下面的圖來看汁讼,當(dāng)我們?cè)O(shè)置了layer的cornerRadius的值,會(huì)對(duì)layer的背景色以及前景框進(jìn)行圓角處理阔墩,當(dāng)我們同時(shí)設(shè)置了layer.masksToBounds=YES的時(shí)候嘿架,contents也會(huì)被切成圓角。

延伸一下:當(dāng)我們對(duì)有圖像內(nèi)容的視圖僅設(shè)置cornerRadius時(shí)啸箫,表象上不生效的原因耸彪,是因?yàn)閏ontents默認(rèn)不會(huì)被切。而對(duì)于單一無組合的簡(jiǎn)單的view忘苛,未設(shè)置圖像的image和button我們也不需要masksToBounds蝉娜。

有了上述知識(shí)儲(chǔ)備,我們來回頭分析一下我們的4組案例扎唾。

  • 案例1:
 //1.按鈕1
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(2, 320, 90, 90);
    btn1.backgroundColor = [UIColor redColor];
    btn1.layer.cornerRadius = 45;
    btn1.layer.masksToBounds = YES;
    [self.view addSubview:btn1];

案例1蜀肘,需要圓角,設(shè)置了圖層的背景顏色稽屏,但并沒有contents扮宠,屬于單一圖層。對(duì)于單一圖層,并不需要開辟離屏緩沖區(qū)坛增,僅在幀緩存內(nèi)直接切圓角就可以了获雕,所以,案例1不會(huì)觸發(fā)離屏渲染收捣。

  • 案例2
    //2.按鈕2
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(96, 320, 90, 90);
    [btn2 setImage:[UIImage imageNamed:@"btn.jpeg"] forState:UIControlStateNormal];
    btn2.layer.cornerRadius = 45;
    btn2.layer.masksToBounds = YES;
    [self.view addSubview:btn2];

案例2届案,需要圓角,設(shè)置了按鈕的圖片內(nèi)容罢艾,沒有設(shè)置背景色或邊框楣颠,但是不要忘了,UIButton如果設(shè)置了image咐蚯,它的內(nèi)部還有一個(gè)UIImageView的圖層童漩,所以屬于組合圖層,當(dāng)需要設(shè)置圓角的時(shí)候春锋,不能一次性在幀緩存中直接讀取矫膨,需要單獨(dú)對(duì)每個(gè)圖層進(jìn)行圓角裁剪,這就需要開辟離屏緩沖區(qū)用于保存期奔,組合之后交給FrameBuffer侧馅,所以案例2會(huì)觸發(fā)離屏渲染。

  • 案例3
    //3.img1
    UIImageView *img1 = [[UIImageView alloc]init];
    img1.frame = CGRectMake(190, 320, 90, 90);
    img1.image = [UIImage imageNamed:@"btn.jpeg"];
    img1.layer.cornerRadius = 45;
    img1.layer.masksToBounds = YES;
    [self.view addSubview:img1];

案例3呐萌,需要圓角馁痴,設(shè)置了imageView的contents,由于layer.masksToBounds = YES;所以圖片將被切成圓角肺孤,由于layer.cornerRadius = 45;所以背景色以及邊框?qū)⒈磺谐蓤A角弥搞,但是并沒有設(shè)置圖層的背景色或邊框,所以案例3還是單一圖層渠旁,不會(huì)觸發(fā)離屏渲染。

  • 案例4
    //4.img2
    UIImageView *img2 = [[UIImageView alloc]init];
    img2.frame = CGRectMake(284, 320, 90, 90);
    img2.image = [UIImage imageNamed:@"btn.jpeg"];
    img2.backgroundColor = [UIColor blueColor];
    img2.layer.cornerRadius = 45;
    img2.layer.masksToBounds = YES;
    [self.view addSubview:img2];

案例4船逮,需要圓角顾腊,同時(shí)設(shè)置了imageView的backgroundColor和contents,屬于組合圖層挖胃,會(huì)觸發(fā)離屏渲染杂靶。

對(duì)于以上4個(gè)案例,我們小結(jié)一下什么情況會(huì)觸發(fā)離屏渲染

1酱鸭、由多個(gè)圖層組成(不止1個(gè))
2吗垮、多個(gè)圖層同時(shí)設(shè)置圓角(layer.cornerRadiuslayer.masksToBounds組合使用)

糾其根本原因:如果你無法僅僅使用frame buffer來畫出最終結(jié)果,需要做額外的計(jì)算凹髓,,那就只能另開一塊內(nèi)存空間來儲(chǔ)存中間結(jié)果烁登。 進(jìn)行離屏渲染

哪些情況會(huì)觸發(fā)離屏渲染

  • 1.需要進(jìn)行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)

iOS9優(yōu)化過后,單純的設(shè)置cornerRadius并不會(huì)觸發(fā)離屏渲染蔚舀。

如果要繪制一個(gè)多圖層且?guī)в袌A角并剪切圓角以外內(nèi)容的容器饵沧,就會(huì)觸發(fā)離屏渲染锨络。

  • 將一個(gè)layer的內(nèi)容裁剪成圓角,可能不存在一次遍歷就能完成的方法
  • 容器的子layer因?yàn)楦溉萜饔袌A角狼牺,那么也會(huì)需要被裁剪羡儿,而這時(shí)它們還在渲染隊(duì)列中排隊(duì),尚未被組合到一塊畫布上是钥,自然也無法統(tǒng)一裁剪

此時(shí)我們就不得不開辟一塊獨(dú)立于frame buffer的空白內(nèi)存掠归。而如果只是設(shè)置cornerRadius(如不需要剪切內(nèi)容,只需要一個(gè)帶圓角的邊框)悄泥,或者只是需要裁掉矩形區(qū)域以外的內(nèi)容(雖然也是剪切虏冻,但是稍微想一下就可以發(fā)現(xiàn),對(duì)于純矩形而言码泞,實(shí)現(xiàn)這個(gè)算法似乎并不需要另開內(nèi)存)兄旬,并不會(huì)觸發(fā)離屏渲染。

  • 2.添加了投影的 layer (layer.shadow)

其原因在于余寥,雖然layer本身是一塊矩形區(qū)域领铐,但是陰影默認(rèn)是作用在其中”非透明區(qū)域“的,而且需要顯示在所有l(wèi)ayer內(nèi)容的下方宋舷,因此根據(jù)畫家算法必須被渲染在先绪撵。但矛盾在于此時(shí)陰影的本體(layer和其子layer)都還沒有被組合到一起,怎么可能在第一步就畫出只有完成最后一步之后才能知道的形狀呢祝蝠?這樣一來又只能另外申請(qǐng)一塊內(nèi)存音诈,把本體內(nèi)容都先畫好,再根據(jù)渲染結(jié)果的形狀绎狭,添加陰影到frame buffer细溅,最后把內(nèi)容畫上去(這只是我的猜測(cè),實(shí)際情況可能更復(fù)雜)儡嘶。

不過如果我們能夠預(yù)先告訴CoreAnimation(通過shadowPath屬性)陰影的幾何形狀喇聊,那么陰影當(dāng)然可以先被獨(dú)立渲染出來,不需要依賴layer本體蹦狂,也就不再需要離屏渲染了誓篱。

使用陰影必須保證 layer 的masksToBounds = false,因此陰影與系統(tǒng)圓角不兼容凯楔。但是注意窜骄,只是在視覺上看不到,對(duì)性能的影響依然摆屯。通常這樣實(shí)現(xiàn)一個(gè)陰影:

let imageViewLayer = avatorView.layer
imageViewLayer.shadowColor = UIColor.blackColor().CGColor
imageViewLayer.shadowOpacity = 1.0 //此參數(shù)默認(rèn)為0邻遏,即陰影不顯示
imageViewLayer.shadowRadius = 2.0 //給陰影加上圓角,對(duì)性能無明顯影響
imageViewLayer.shadowOffset = CGSize(width: 5, height: 5)
//設(shè)定路徑:與視圖的邊界相同
let path = UIBezierPath(rect: cell.imageView.bounds)
imageViewLayer.shadowPath = path.CGPath//路徑默認(rèn)為 nil
  • 3.設(shè)置了組透明度為 YES,并且透明度不為 1 的 layer (layer.allowsGroupOpacity/ layer.opacity)

alpha并不是分別應(yīng)用在每一層之上党远,而是只有到整個(gè)layer樹畫完之后削解,再統(tǒng)一加上alpha,最后和底下其他layer的像素進(jìn)行組合沟娱。顯然也無法通過一次遍歷就得到最終結(jié)果氛驮。將一對(duì)藍(lán)色和紅色layer疊在一起,然后在父layer上設(shè)置opacity=0.5济似,并復(fù)制一份在旁邊作對(duì)比矫废。左邊關(guān)閉group opacity,右邊保持默認(rèn)(從iOS7開始砰蠢,如果沒有顯式指定蓖扑,group opacity會(huì)默認(rèn)打開),然后打開offscreen rendering的調(diào)試台舱,我們會(huì)發(fā)現(xiàn)右邊的那一組確實(shí)是離屏渲染了律杠。

CALayer的allowsGroupOpacity屬性,UIView 的alpha屬性等同于 CALayer opacity屬性竞惋。GroupOpacity=YES柜去,子layer 在視覺上的透明度的上限是其父 layer 的opacity。

這個(gè)屬性的文檔說明:

The default value is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist file. If no value is found, the default value is YES for apps linked against the iOS 7 SDK or later and NO for apps linked against an earlier SDK.

從 iOS 7 以后默認(rèn)全局開啟了這個(gè)功能拆宛,這樣做是為了讓子視圖與其容器視圖保持同樣的透明度嗓奢。

GroupOpacity 開啟離屏渲染的條件是:layer.opacity != 1.0并且有子 layer 或者背景圖。

當(dāng)父視圖的layer.opacity != 1.0時(shí)浑厚,會(huì)開啟離屏渲染股耽,opacity并不是分別應(yīng)用在每一層之上,而是只有到整個(gè)layer樹畫完之后钳幅,再統(tǒng)一加上opacity物蝙,最后和底下其他layer的像素進(jìn)行組合

當(dāng)父視圖的layer.opacity == 1.0時(shí),父視圖不用管子視圖敢艰,只需顯示當(dāng)前視圖即可诬乞。
為了讓子視圖與父視圖保持同樣的透明度,從 iOS 7 以后默認(rèn)全局開啟了這個(gè)功能盖矫。我們可以設(shè)置layer的opacity值為YES,減少?gòu)?fù)雜圖層合成

  • 4.使用了 mask 的 layer (layer.mask)
WWDC中蘋果的解釋击奶,mask需要遍歷至少三次!

mask是應(yīng)用在layer和其所有子layer的組合之上的辈双,而且可能帶有透明度,只有到整個(gè)layer樹畫完之后,再統(tǒng)一加上mask柜砾,最后和底下其他layer的像素進(jìn)行組合湃望。(和group opacity的原理類似)

  • 5.毛玻璃效果
UIBlurEffect

渲染的位圖不能直接交給FrameBuffer等待顯示,需要經(jīng)過模糊處理之后才能將最終的結(jié)果交給FrameBuffer。

  • 6.采用了光柵化的 layer (layer.shouldRasterize)

如果開啟证芭,在觸發(fā)離屏繪制的同時(shí)瞳浦,會(huì)將光柵化后的內(nèi)容緩存起來,如果對(duì)應(yīng)的layer及其sublayers沒有發(fā)生改變废士,在下一幀的時(shí)候可以直接復(fù)用叫潦。這將在很大程度上提升渲染性能。

使用光柵化時(shí)官硝,可以開啟“Color Hits Green and Misses Red”來檢查該場(chǎng)景下光柵化操作是否是一個(gè)好的選擇矗蕊。綠色表示緩存被復(fù)用,紅色表示緩存在被重復(fù)創(chuàng)建氢架。

關(guān)于光柵化的使用建議:

  • 如果layer不能被復(fù)用傻咖,則不建議開啟
  • 如果layer不是靜態(tài)的,需要頻繁的被修改(如處在動(dòng)畫之中岖研,imageview的image需要改變卿操,label的text會(huì)發(fā)生改變等)此時(shí)開啟光柵化反而影響效率
  • 離屏渲染有時(shí)間的限制,緩存內(nèi)容如果在100ms內(nèi)沒有被復(fù)用孙援,則會(huì)被丟棄害淤,無法進(jìn)行復(fù)用
  • 離屏渲染的緩存空間也是有限的,超過屏幕像素大小的2.5倍就會(huì)失效赃磨。
  • 7.繪制了文字的 layer (UILabel, CATextLayer, Core Text 等)

UILabel 和 UITextView 要想顯示圓角需要表現(xiàn)出與周圍不同的背景色才行筝家。想要在 UILabel 和 UITextView 上實(shí)現(xiàn)低成本的圓角(不觸發(fā)離屏渲染),需要保證 layer 的contents呈現(xiàn)透明的背景色邻辉,文本視圖類的 layer 的contents默認(rèn)是透明的(字符就在這個(gè)透明的環(huán)境里繪制溪王、顯示),此時(shí)只需要設(shè)置 layer 的backgroundColor值骇,再加上cornerRadius就可以搞定了莹菱。不過 UILabel 上設(shè)置backgroundColor的行為被更改了,不再是設(shè)定 layer 的背景色而是為contents設(shè)置背景色吱瘩,UITextView 則沒有改變這一點(diǎn)道伟,所以在 UILabel 上實(shí)現(xiàn)圓角要這么做:

//不要這么做:label.backgroundColor = aColor 以及不要在 IB 里為 label 設(shè)置背景色
label.layer.backgroundColor = aColor
label.layer.cornerRadius = 5
  • 8.edge antialiasing(抗鋸齒)

設(shè)置 allowsEdgeAntialiasing 屬性為YES(默認(rèn)為NO).

經(jīng)過測(cè)試,開啟 edge antialiasing(旋轉(zhuǎn)視圖并且設(shè)置layer.allowsEdgeAntialiasing = true) 在 iOS 8 和 iOS 9 上并不會(huì)觸發(fā)離屏渲染使碾,對(duì)性能也沒有什么影響蜜徽,也許到現(xiàn)在這個(gè)功能已經(jīng)被優(yōu)化了。

特殊的離屏渲染:CPU的“離屏渲染”

如果我們?cè)赨IView中實(shí)現(xiàn)了drawRect方法票摇,就算它的函數(shù)體內(nèi)部實(shí)際沒有代碼拘鞋,系統(tǒng)也會(huì)為這個(gè)view申請(qǐng)一塊內(nèi)存區(qū)域,等待CoreGraphics可能的繪畫操作矢门。

對(duì)于類似這種“新開一塊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不擅長(zhǎng)做這件事驾霜,所以我們需要盡量避免它,就誤以為這就是需要避免離屏渲染的原因买置。但是根據(jù)蘋果工程師的說法粪糙,CPU渲染并非真正意義上的離屏渲染。另一個(gè)證據(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的操作是高度流水線化的伐弹。本來所有計(jì)算工作都在有條不紊地正在向frame buffer輸出,此時(shí)突然收到指令榨为,需要輸出到另一塊內(nèi)存惨好,那么流水線中正在進(jìn)行的一切都不得不被丟棄,切換到只能服務(wù)于我們當(dāng)前的“切圓角”操作随闺。等到完成以后再次清空日川,再回到向frame buffer輸出的正常流程。

由于離屏渲染中的離屏緩沖區(qū)矩乐,是 額外開辟的一個(gè)存儲(chǔ)空間龄句,當(dāng)它將數(shù)據(jù)轉(zhuǎn)存到Frame Buffer時(shí),也是需要耗費(fèi)時(shí)間的散罕,所以在轉(zhuǎn)存的過程中分歇,仍有掉幀的可能。

在tableView或者collectionView中欧漱,滾動(dòng)的每一幀變化都會(huì)觸發(fā)每個(gè)cell的重新繪制职抡,因此一旦存在離屏渲染,上面提到的上下文切換就會(huì)每秒發(fā)生60次硫椰,并且很可能每一幀有幾十張的圖片要求這么做繁调,對(duì)于GPU的性能沖擊可想而知(GPU非常擅長(zhǎng)大規(guī)模并行計(jì)算,但是我想頻繁的上下文切換顯然不在其設(shè)計(jì)考量之中)

開辟額外的空間
上下文切換

那為什么我們明知有性能問題時(shí)靶草,還是要使用離屏渲染呢蹄胰?

離屏渲染是一種手段,設(shè)計(jì)的目的本身是解決問題的奕翔。離屏緩存區(qū)是一個(gè)臨時(shí)的緩沖區(qū)裕寨,用來存放在后續(xù)操作使用,但目前并不使用的數(shù)據(jù)派继。

1.一些特殊效果需要使用額外的 Offscreen Buffer 來保存渲染的中間狀態(tài)宾袜,所以不得不使用離屏渲染。這種情況下的離屏渲染是系統(tǒng)自動(dòng)觸發(fā)的驾窟,例如經(jīng)常使用的圓角庆猫、陰影、高斯模糊等绅络。

2.處于效率目的月培,可以將內(nèi)容提前渲染保存在 Offscreen Buffer 中,達(dá)到復(fù)用的目的恩急。這種情況開發(fā)者通過 CALayer 的 shouldRasterize 主動(dòng)觸發(fā)的杉畜。

盡管離屏渲染開銷很大,但是當(dāng)我們無法避免它的時(shí)候衷恭,可以想辦法把性能影響降到最低此叠。優(yōu)化思路也很簡(jiǎn)單:既然已經(jīng)花了不少精力把圖片裁出了圓角,如果我能把結(jié)果緩存下來随珠,那么下一幀渲染就可以復(fù)用這個(gè)成果灭袁,不需要再重新畫一遍了。

CALayer為這個(gè)方案提供了對(duì)應(yīng)的解法:shouldRasterize牙丽。一旦被設(shè)置為true简卧,Render Server就會(huì)強(qiáng)制把layer的渲染結(jié)果(包括其子layer,以及圓角烤芦、陰影举娩、group opacity等等)保存在一塊內(nèi)存中,這樣一來在下一幀仍然可以被復(fù)用构罗,而不會(huì)再次觸發(fā)離屏渲染铜涉。有幾個(gè)需要注意的點(diǎn):

  • shouldRasterize的主旨在于降低性能損失,但總是至少會(huì)觸發(fā)一次離屏渲染遂唧。如果你的layer本來并不復(fù)雜芙代,也沒有圓角陰影等等,打開這個(gè)開關(guān)反而會(huì)增加一次不必要的離屏渲染
  • 離屏渲染緩存有空間上限盖彭,最多不超過屏幕總像素的2.5倍大小
  • 一旦緩存超過100ms沒有被使用纹烹,會(huì)自動(dòng)被丟棄
  • layer的內(nèi)容(包括子layer)必須是靜態(tài)的页滚,因?yàn)橐坏┌l(fā)生變化(如resize,動(dòng)畫)铺呵,之前辛苦處理得到的緩存就失效了裹驰。如果這件事頻繁發(fā)生,我們就又回到了“每一幀都需要離屏渲染”的情景片挂,而這正是開發(fā)者需要極力避免的幻林。針對(duì)這種情況,Xcode提供了“Color Hits Green and Misses Red”的選項(xiàng)音念,幫助我們查看緩存的使用是否符合預(yù)期
  • 其實(shí)除了解決多次離屏渲染的開銷沪饺,shouldRasterize在另一個(gè)場(chǎng)景中也可以使用:如果layer的子結(jié)構(gòu)非常復(fù)雜,渲染一次所需時(shí)間較長(zhǎng)闷愤,同樣可以打開這個(gè)開關(guān)整葡,把layer繪制到一塊緩存,然后在接下來復(fù)用這個(gè)結(jié)果讥脐,這樣就不需要每次都重新繪制整個(gè)layer樹了掘宪。

如何避免圓角的離屏渲染

圓角引起離屏渲染的本質(zhì)是裁剪的疊加,導(dǎo)致 masksToBounds 對(duì) layer 以及所有 sublayer 進(jìn)行二次處理攘烛。那么我們只要避免使用 masksToBounds 進(jìn)行二次處理魏滚,而是對(duì)所有的 sublayer 進(jìn)行預(yù)處理,就可以只進(jìn)行“畫家算法”坟漱,用一次疊加就完成繪制鼠次。

可以用以下方案代替直接設(shè)置圓角的操作

  • 【換資源】直接更換資源,讓UI提供帶圓角的圖片芋齿。
  • 【mask】使用layer.mask屬性腥寇,增加一個(gè)和背景色相同的mask覆蓋在最上層,蓋住四個(gè)角觅捆,營(yíng)造出圓角的形狀赦役。但這種方式難以解決背景色為圖片或漸變色的情況。
  • 【UIBezierPath】使用貝塞爾曲線繪制閉合圓角的矩形栅炒,在上下文中設(shè)置只有內(nèi)部可見掂摔,再將不帶圓角的 layer 渲染成圖片,添加到貝塞爾矩形中赢赊。這種方法效率更高乙漓,但是 layer 的布局一旦改變,貝塞爾曲線都需要手動(dòng)地重新繪制释移,所以需要對(duì) frame叭披、color 等進(jìn)行手動(dòng)地監(jiān)聽并重繪。
  • 【CoreGraphics】重寫drawRect:玩讳,用 CoreGraphics 相關(guān)方法涩蜘,在需要應(yīng)用圓角時(shí)進(jìn)行手動(dòng)繪制嚼贡。不過 CoreGraphics 效率也很有限,如果需要多次調(diào)用也會(huì)有效率問題同诫。
  • 【CoreGraphics】方式 用貝塞爾曲線UIBezierPath和Core Graphics框架畫出一個(gè)圓角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100,100,100,100)];

imageView.image = [UIImage imageNamed:@"xx"];

//開始對(duì)imageView進(jìn)行畫圖

UIGraphicsBeginImageContextWithOptions(imageView.bounds.size,NO,1.0);

//使用貝塞爾曲線畫出一個(gè)圓形圖

[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];

[imageView drawRect:imageView.bounds];

imageView.image = UIGraphicsGetImageFromCurrentImageContext();

//結(jié)束畫圖

UIGraphicsEndImageContext();

[self.view addSubview:imageView];

  • 【CAShapeLayer 】方式

使用CAShapeLayer(屬于CoreAnimation)與貝塞爾曲線可以實(shí)現(xiàn)不在view的drawRect方法中畫出一些想要的圖形编曼,CAShapeLayer動(dòng)畫渲染直接提交GPU當(dāng)中,相較于view的drawRect方法使用CPU渲染而言剩辟,其效率高,能大大優(yōu)化內(nèi)存使用

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; 
imageView.image = [UIImage imageNamed:@"myImg"]; 
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init]; 
//設(shè)置大小 
maskLayer.frame = imageView.bounds; 
//設(shè)置圖形樣子 
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer; 
[self.view addSubview:imageView];

  • 【參考yyimage的寫法】
- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius 
 corners:(UIRectCorner)corners 
 borderWidth:(CGFloat)borderWidth 
 borderColor:(UIColor *)borderColor 
 borderLineJoin:(CGLineJoin)borderLineJoin { 
 if (corners != UIRectCornerAllCorners) { 
 UIRectCorner tmp = 0; 
 if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft; 
 if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight; 
 if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft; 
 if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight; 
 corners = tmp; 
 } 
 UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); 
 CGContextRef context = UIGraphicsGetCurrentContext(); 
 CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); 
 CGContextScaleCTM(context, 1, -1); 
 CGContextTranslateCTM(context, 0, -rect.size.height); 
 CGFloat minSize = MIN(self.size.width, self.size.height); 
 if (borderWidth < minSize / 2) { 
 UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners 
cornerRadii:CGSizeMake(radius, borderWidth)]; 
 [path closePath]; 
 CGContextSaveGState(context); 
 [path addClip]; 
 CGContextDrawImage(context, rect, self.CGImage); 
 CGContextRestoreGState(context); 
 } 
 if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) { 
 CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale; 
 CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset); 
 CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0; 
 UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, 
borderWidth)]; 
 [path closePath]; 
 path.lineWidth = borderWidth; 
 path.lineJoinStyle = borderLineJoin; 
 [borderColor setStroke]; 
}

  • 為UIImage類擴(kuò)展一個(gè)實(shí)例函數(shù)往扔,仿YYImage做法
- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{
    /* 當(dāng)前UIImage的可見繪制區(qū)域 */
    CGRect rect = (CGRect){0.f,0.f,size};
    /* 創(chuàng)建基于位圖的上下文 */
    UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
    /* 在當(dāng)前位圖上下文添加圓角繪制路徑 */
    CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
    /* 當(dāng)前繪制路徑和原繪制路徑相交得到最終裁剪繪制路徑 */
    CGContextClip(UIGraphicsGetCurrentContext());
    /* 繪制 */
    [self drawInRect:rect];
    /* 取得裁剪后的image */
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    /* 關(guān)閉當(dāng)前位圖上下文 */
    UIGraphicsEndImageContext();
    return image;
}

使用方法

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(10, 10, 100, 100)];
/* 創(chuàng)建并初始化UIImage */
UIImage *image = [UIImage imageNamed:@"icon"];
/* 添加圓角矩形 */
image = [image imageWithCornerRadius:50 ofSize:imageView.frame.size];
[imageView setImage:image];

參考閱讀:

關(guān)于iOS離屏渲染的深入研究
繪制像素到屏幕上
離屏渲染優(yōu)化詳解:實(shí)例示范+性能測(cè)試
iOS 渲染原理解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贩猎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子萍膛,更是在濱河造成了極大的恐慌吭服,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝗罗,死亡現(xiàn)場(chǎng)離奇詭異艇棕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)串塑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門沼琉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人桩匪,你說我怎么就攤上這事打瘪。” “怎么了傻昙?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵闺骚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我妆档,道長(zhǎng)僻爽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任贾惦,我火速辦了婚禮胸梆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘须板。我一直安慰自己乳绕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布逼纸。 她就那樣靜靜地躺著洋措,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杰刽。 梳的紋絲不亂的頭發(fā)上菠发,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天王滤,我揣著相機(jī)與錄音,去河邊找鬼滓鸠。 笑死雁乡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的糜俗。 我是一名探鬼主播踱稍,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼悠抹!你這毒婦竟也來了珠月?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤楔敌,失蹤者是張志新(化名)和其女友劉穎啤挎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卵凑,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡庆聘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡菇晃,死狀恐怖故河,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站酱塔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏危虱。R本人自食惡果不足惜羊娃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埃跷。 院中可真熱鬧蕊玷,春花似錦、人聲如沸弥雹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)剪勿。三九已至贸诚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酱固。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工械念, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人运悲。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓龄减,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親班眯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子希停,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353