很多人都知道設(shè)置了layer的圓角屬性cornerRadius并裁減clipsToBounds/layer.masksToBounds = YES之后會(huì)觸發(fā)離屏渲染,在類似tableView這種在整個(gè)界面上呈現(xiàn)出很多圓角視圖就會(huì)造成卡頓,所以不建議在一個(gè)頁面上用這種方式大量設(shè)置圓角,但是所有的圓角都會(huì)觸發(fā)離屏渲染嗎?我們來看看下面的例子。
-
先通過Xcode模擬器打開離屏渲染效果.
真機(jī)調(diào)試則跑起來后在Xcode頂部菜單選擇Debug--ViewDebugging--Rendering--Color OffScreen-Rendered Yellow開啟。
- 我們來看下下面代碼跑起來的效果
///都用圓角
//imageView沒有設(shè)置背景色,不會(huì)離屏
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 30, 100, 100)];
imgView.layer.cornerRadius = 20;
imgView.layer.masksToBounds = YES;
imgView.image = [UIImage imageNamed:@"測(cè)試.png"];
[self.view addSubview:imgView];
//imageView設(shè)置背景色,用clipsToBounds/masksToBounds會(huì)離屏
UIImageView *imgView2 = [[UIImageView alloc] initWithFrame:CGRectMake(100, 150, 100, 100)];
imgView2.layer.cornerRadius = 20;
// imgView2.clipsToBounds = YES;
imgView2.layer.masksToBounds = YES;
imgView2.backgroundColor = [UIColor whiteColor];
imgView2.image = [UIImage imageNamed:@"測(cè)試.png"];
[self.view addSubview:imgView2];
//imageView設(shè)置邊框,會(huì)離屏
UIImageView *imgView3 = [[UIImageView alloc] initWithFrame:CGRectMake(100, 270, 100, 100)];
imgView3.layer.cornerRadius = 20;
imgView3.layer.masksToBounds = YES;
imgView3.image = [UIImage imageNamed:@"測(cè)試.png"];
imgView3.layer.borderWidth = 2;
imgView3.layer.borderColor = [UIColor redColor].CGColor;
[self.view addSubview:imgView3];
//按鈕只設(shè)置背景色,不會(huì)離屏
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(100, 400, 100, 100);
btn.layer.cornerRadius = 20;
btn.layer.masksToBounds = YES;
btn.backgroundColor = [UIColor redColor];
[self.view addSubview:btn];
//按鈕設(shè)置背景圖,會(huì)離屏
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 520, 100, 100);
[btn1 setImage:[UIImage imageNamed:@"測(cè)試.png"] forState:UIControlStateNormal];
btn1.layer.cornerRadius = 20;
btn1.layer.masksToBounds = YES;
[self.view addSubview:btn1];

- 可以看到黃色的區(qū)域發(fā)生了離屏渲染,分別是第2、3、5個(gè)視圖,而第1、4個(gè)正常。說明了離屏渲染的觸發(fā)是有條件的。
我們先來了解下屏幕的渲染流程,下面分別是正常渲染流程和離屏渲染流程

蘋果使用了CPU繪制和GPU繪制這兩種不同的機(jī)制處理,單個(gè)視圖的繪制可能同時(shí)需要這兩種機(jī)制。這兩種機(jī)制有不同的性能考慮。
CPU“離屏渲染”
當(dāng)你實(shí)現(xiàn)了drawRect并使用CoreGraphics,或者使用CoreText繪圖,也會(huì)觸發(fā)CPU"離屏渲染”,這種方式是CPU去同步繪圖,它將bits寫入位圖緩沖區(qū)。
這種新開一塊CGContext來畫圖的操作,沒有直接將像素?cái)?shù)據(jù)放到 frame buffer,而是暫時(shí)放到了CGContext。進(jìn)一步來說,其實(shí)所有CPU進(jìn)行的光柵化操作(如文字渲染、圖片解碼),都無法直接繪制到由GPU掌管的frame buffer,只能暫時(shí)先放在另一塊內(nèi)存之中,說起來都屬于“離屏渲染"。
其實(shí)通過CPU渲染就是俗稱的“軟件渲染”,而真正的離屏渲染發(fā)生在GPU。
不信的話,你此時(shí)打開Xcode調(diào)試的“Color offscreen rendered yellow”開關(guān),你會(huì)發(fā)現(xiàn)這片區(qū)域不會(huì)被標(biāo)記為黃色,說明Xcode并不認(rèn)為這屬于離屏渲染。
GPU離屏渲染
真正的離屏渲染指的是在渲染服務(wù)器(一個(gè)獨(dú)立的進(jìn)程)中進(jìn)行的,并通過GPU執(zhí)行。當(dāng)OpenGL(現(xiàn)在是Metal)渲染器去繪制每一層時(shí),它可能不得不停止一些子層次結(jié)構(gòu),并將它們組合到一個(gè)單獨(dú)的緩沖區(qū)中。
假設(shè)我們的屏幕刷新率為60FPS,也就是一秒鐘能顯示60幀,那么在正常渲染流程里,會(huì)先把圖像數(shù)據(jù)放在幀緩沖區(qū)(Frame Buffer)里面,然后屏幕刷新的時(shí)候視頻控制器不斷從幀緩沖區(qū)里取數(shù)據(jù)。
而GPU離屏渲染的時(shí)候,會(huì)先把CPU處理好的數(shù)據(jù)放在幀緩沖區(qū)之外另外開辟的離屏緩沖區(qū)(OffScreen Buffer)里面,等把要一起顯示的數(shù)據(jù)都疊加到離屏緩沖區(qū)里之后,再交由幀緩沖區(qū)按正常渲染流程進(jìn)行。
舉兩個(gè)例子:
- 當(dāng)我們使用遮罩效果
layer.mask的時(shí)候,也會(huì)觸發(fā)離屏渲染,它的渲染過程中,GPU首先渲染好遮罩層layer,這時(shí)候并不能交給幀緩沖區(qū)給屏幕顯示,需要等到整個(gè)圖像遮罩效果處理后才能交給幀緩沖區(qū)。這時(shí)候它就把遮罩層放在離屏緩沖區(qū),然后再渲染好另外一個(gè)圖層,之后也放到離屏緩沖區(qū)里,兩個(gè)圖層合并之后再交給幀緩沖區(qū),最后顯示到屏幕上。而這一次mask發(fā)生了兩次離屏渲染和一次主屏渲染,相當(dāng)于普通視圖的3倍,若再加上下文環(huán)境切換,一次layer.mask就是普通渲染的30倍以上耗時(shí)操作,原因下面會(huì)解釋。
我做了個(gè)測(cè)試來直觀感受使用遮罩的性能損耗。
當(dāng)使用layer.cornerRadius+layer.masksToBounds處理圓角視圖,在滾動(dòng)視圖里快速滾動(dòng)時(shí),可以看到處于中間的FPS變化在56左右,前面幾秒是debug模式應(yīng)用啟動(dòng),幀率比較低不用在意。

而如果改為使用
UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:byRoundingCorners cornerRadii:cornerRadius];
CAShapeLayer* shape = [CAShapeLayer layer];
shape.path = path.CGPath;
self.layer.mask = shape;

可以看到FPS已經(jīng)低于50了。所以盡量不要過多的使用layer.mask去處理視圖,特別是頻繁滾動(dòng)的列表視圖。
- 當(dāng)我們使用毛玻璃效果
UIVisualEffectView,渲染流程是先拿到渲染內(nèi)容(Render Content)、捕獲內(nèi)容(capture Content)、垂直模糊(Vertical Blur)、水平模糊(Horizontal Blur),分別把這4步的圖層放到離屏緩沖區(qū)里,然后拿出來合成(Compositing Pass)最終的模糊效果圖。
所以離屏渲染的原理是:APP進(jìn)行額外的渲染和合并操作,在離屏緩沖區(qū)(OffScreen Buffer)組合,之后交給幀緩沖區(qū)(Frame Buffer),最后顯示到屏幕上。
這樣(觸發(fā)離屏渲染)帶來的影響是什么呢?
1.需要開辟額外的存儲(chǔ)空間;
2.從Frame Buffer切換到OffScreen Buffer,再從OffScreen Buffer轉(zhuǎn)存到Frame Buffer,上下文環(huán)境的切換需要時(shí)間;
這就解釋了為什么離屏渲染這么耗時(shí)。原因主要在創(chuàng)建緩沖區(qū)和上下文切換。創(chuàng)建新的緩沖區(qū)代價(jià)都不算大,付出最大代價(jià)的是上下文切換。
上下文切換:首先我要保存當(dāng)前屏幕渲染環(huán)境,然后切換到一個(gè)新的繪制環(huán)境,申請(qǐng)繪制資源,初始化環(huán)境,然后開始一個(gè)繪制,繪制完畢后銷毀這個(gè)繪制環(huán)境,例如需要切換到On-Screen Rendering或者再開始一個(gè)新的離屏渲染重復(fù)之前的操作。
所以開啟了大量的離屏渲染就會(huì)容易掉幀,造成卡頓。
既然離屏渲染有這些性能問題,那為什么還要用呢?
- 當(dāng)我們需要一些特殊的效果,這種效果不能一次性渲染完成,需要使用離屏緩沖區(qū)來保存中間狀態(tài),就不得不使用離屏渲染,這種情況是系統(tǒng)自動(dòng)觸發(fā),比如經(jīng)常使用的圓角、陰影、高斯模糊、遮罩(
mask),抗鋸齒(edge antialiasing)等。 - 可以提升渲染的效率,當(dāng)一個(gè)效果無法避免觸發(fā)離屏渲染,那么可以緩存這個(gè)結(jié)果并復(fù)用,來降低性能影響。這種情況是需要我們手動(dòng)觸發(fā)的,也就是開啟光柵化。
主動(dòng)使用離屏渲染的原因:光柵化
上面說到光柵化(shouldRasterize = YES)可以會(huì)將處理渲染后的視圖緩存復(fù)用,提高性能。
使用方法:
view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = UIScreen.mainScreen.scale;

但是光柵化的開啟并不一定會(huì)帶來好處,使用建議:
- 如果layer不是靜態(tài),需要被頻繁修改,比如處于動(dòng)畫中、
size修改、tableView、collectionView視圖中,那么開啟光柵化反而影響了效率; - 離屏渲染緩存內(nèi)容有時(shí)間限制,緩存的內(nèi)容如果
100ms內(nèi)沒有被使用,那么它就會(huì)丟棄,無法進(jìn)行復(fù)用; - 離屏渲染的緩存空間有限,大小相當(dāng)于屏幕像素點(diǎn)的2.5倍,超過的話也會(huì)失效,無法進(jìn)行復(fù)用了;
總結(jié)來說,如果可以避免觸發(fā)離屏渲染,盡量避免,否則如果視圖可以復(fù)用并且是靜態(tài)內(nèi)容(也就是內(nèi)部結(jié)構(gòu)和內(nèi)容不發(fā)生變化的視圖),才考慮開啟光柵化,但是也要考慮光柵緩存的利用率,如果整屏視圖中利用到光柵緩存的視圖很少,那反而更耗費(fèi)性能。(可以通過CoreAnimation InStruments工具查看并通過Xcode打開「Color Hits Green and Misses Red」觀察離屏渲染對(duì)緩存的使用,綠色代表了用到了光柵緩存,紅色則是需要重新渲染的部分,紅色意味著柵格化反而有負(fù)面的性能影響了)
回到一開始的問題,設(shè)置圓角而觸發(fā)離屏渲染是有條件的。
先來看看一個(gè)視圖的渲染層級(jí)。

再來看看蘋果官方文檔上關(guān)于設(shè)置圓角的說法。

可以看到,蘋果告訴我們,設(shè)置了cornerRadius只會(huì)設(shè)置backgroundColor和border的圓角,而內(nèi)容圖層contents,并不會(huì)設(shè)置圓角。除非你同時(shí)設(shè)置了layer.masksToBounds(對(duì)應(yīng)view.clipsToBounds),才會(huì)也對(duì)contents設(shè)置圓角。
離屏渲染的邏輯

這張圖演示的是著名的油畫算法。指的是先繪制遠(yuǎn)的部分,再繪制近的部分。我們圖層的渲染也是同理,系統(tǒng)會(huì)先繪制最底層的圖層,再往上一層層的繪制,最后形成了圖層樹,蘋果建議開發(fā)者在建立UI的時(shí)候,圖層樹不要太復(fù)雜,層級(jí)不要太多,不然也是會(huì)有性能影響。
- 正常渲染:如果是不觸發(fā)離屏渲染的正常渲染,蘋果在繪制完最底層的圖層,從幀緩沖區(qū)顯示到屏幕上之后,就丟棄了,并不會(huì)保存起來,再畫第二層,也是如此,顯示完了就丟棄,從而節(jié)省了空間。
- 離屏渲染:如果對(duì)一個(gè)多圖層圖像進(jìn)行圓角處理,就需要對(duì)所有圖層進(jìn)行圓角(包括內(nèi)容
contents),如果按照正常渲染,一層用完就丟棄,這樣就達(dá)不到顯示的效果。這時(shí)候就需要開辟一個(gè)離屏緩沖區(qū)去保存這些圖層,等到所有圖層都做了圓角處理,就把它們從離屏緩沖區(qū)里取出來進(jìn)行合并顯示。
這就解釋了為什么有些圓角會(huì)觸發(fā)離屏渲染,糾其根本就是用到了離屏緩沖區(qū)。
我總結(jié)了處理圓角的幾種可用方式.
1.最簡單的就是讓UI同事切一個(gè)帶圓角的圖。
- 如果內(nèi)容
contents沒有內(nèi)容,只有背景色,就不用使用masksToBounds/ClipsToBounds了。直接設(shè)置cornerRadius就好了。
3.設(shè)置imageView的圓角并裁減。比如我們要設(shè)置帶圖片UIButton的圓角,可以這樣設(shè)置。
//按鈕設(shè)置背景圖,會(huì)離屏
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 100, 100, 100);
[btn1 setImage:[UIImage imageNamed:@"測(cè)試.png"] forState:UIControlStateNormal];
btn1.layer.cornerRadius = 20;
btn1.layer.masksToBounds = YES;
[self.view addSubview:btn1];
//改為對(duì)button上的imageView裁剪圓角
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, 220, 100, 100);
[btn2 setImage:[UIImage imageNamed:@"測(cè)試.png"] forState:UIControlStateNormal];
btn2.imageView.layer.cornerRadius = 20;
btn2.imageView.layer.masksToBounds = YES;
[self.view addSubview:btn2];
這樣就不會(huì)離屏渲染了。

- 4.
CATextLayer,Core Text設(shè)置了文本內(nèi)容也會(huì)觸發(fā)離屏渲染,而label設(shè)置圓角不當(dāng)?shù)臅r(shí)候也會(huì)觸發(fā)離屏渲染
//文字圓角
UILabel *lab = [[UILabel alloc] initWithFrame:CGRectMake(220, 30, 50, 50)];
lab.text = @"文字";
//設(shè)置layer的背景色會(huì)離屏渲染
lab.layer.backgroundColor = [UIColor redColor].CGColor;
//設(shè)置label背景色不會(huì)離屏渲染
// lab.backgroundColor = [UIColor redColor];
lab.layer.cornerRadius = 20;
//當(dāng)設(shè)置layer背景色時(shí),圓角不用裁剪也能生效。
lab.layer.masksToBounds = YES;
//在masksToBounds基礎(chǔ)上設(shè)置了邊框就會(huì)導(dǎo)致離屏
// lab.layer.borderWidth = 1;
[self.view addSubview:lab];
//添加子視圖后只要有裁剪,不論是在設(shè)置哪里的背景色都會(huì)離屏。
// UIView *viewinLab = [[UIView alloc] initWithFrame:CGRectMake(30, 0, 20, 20)];
// viewinLab.backgroundColor = [UIColor blueColor];
// [lab addSubview:viewinLab];

a. 會(huì)觸發(fā)離屏渲染的情況:前提條件:cornerRadius+masksToBounds,加上以下任一條件。
1??設(shè)置了layer.backgroundColor
2??設(shè)置了邊框
3??添加子視圖
b. 不會(huì)觸發(fā)離屏渲染的情況:
cornerRadius+masksToBounds+label.backgroundColor,或者cornerRadius+layer.backgroundColor(加上masksToBounds會(huì)離屏渲染)。
總結(jié):
圓角處理會(huì)導(dǎo)致離屏渲染的根本原因就是對(duì)多層進(jìn)行裁剪,用到了離屏緩沖區(qū)。單純cornerRadius+layer.backgroundColor設(shè)置圓角只會(huì)對(duì)contents圓角,在此之上加了masksToBounds,則會(huì)對(duì)contents和contents下面背景色層裁剪,所以是多層裁剪。而改為label.backgroundColor,masksToBounds則只會(huì)對(duì)contents層裁剪圓角,不存在layer的背景色層,所以不會(huì)觸發(fā)離屏渲染。
建議使用方式
用cornerRadius+layer.backgroundColor方式對(duì)label設(shè)置圓角,或者cornerRadius+label.backgroundColor+ masksToBounds設(shè)置圓角。
- 使用
YY_Image的處理方式
這個(gè)方法里還可以接收邊框設(shè)置,需要的時(shí)候可以一步到位。也可以自己提取里面的圓角處理方式使用。
- (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];
[path stroke];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
6.如果不是圖片,處理指定部分圓角,可以采用UIBezierPath+CAShapeLayer,將layer添加到view上面,而不使用mask。
UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:byRoundingCorners cornerRadii:cornerRadius];
CAShapeLayer* shape = [CAShapeLayer layer];
shape.path = path.CGPath;
//bgColor為視圖背景色
shape.fillColor = bgColor.CGColor;
shape.strokeColor = UIColor.blueColor.CGColor;
[self.layer insertSublayer:shape atIndex:0];
- 采用
CGContext異步渲染,CPU繪圖的方式,GPU使用率低。使用的時(shí)候可以放在后臺(tái)繪制,這樣CPU使用率低很多,幀率提高很多。
第一種方式:不使用貝塞爾曲線,只使用CGContext,所以不能指定部分圓角。
let img = UIImageView(frame: CGRect(x: 100, y: 20, width: 20, height: 20))
img.image = UIImage(named: "11")
navigationBar.addSubview(img)
let frame = img.bounds
UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
let ctx = UIGraphicsGetCurrentContext()
ctx?.addEllipse(in: frame)
ctx?.clip()
img.draw(frame)
let ig = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
img.image = ig
第二種方式:處理UIImage,能指定部分圓角。
let img = UIImageView(frame: CGRect(x: 100, y: 20, width: 20, height: 20))
img.image = UIImage(named: "11")
navigationBar.addSubview(img)
let frame = img.bounds
UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
UIBezierPath.init(roundedRect: frame, byRoundingCorners: [.topRight], cornerRadii: CGSize(width: 10, height: 10)).addClip()
img.draw(frame)
let ig = UIGraphicsGetImageFromCurrentImageContext()
img.image = ig
UIGraphicsEndImageContext()
第三種方式:視圖渲染1,能指定部分圓角,適用所有視圖
let frame = img.bounds
UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
UIBezierPath.init(roundedRect: frame, byRoundingCorners: [.topRight], cornerRadii: CGSize(width: 10, height: 10)).addClip()
img.layer.render(in: UIGraphicsGetCurrentContext()!)
let ig = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
img.layer.contents = ig?.cgImage
第四種方式:視圖渲染2,能指定部分圓角,適用所有視圖
let renderer = UIGraphicsImageRenderer(size: size)
let frame = img.bounds
img.layer.contents = renderer.image { rendererContext in
UIBezierPath.init(roundedRect: frame, byRoundingCorners: [.topRight], cornerRadii: CGSize(width: 10, height: 10)).addClip()
img.layer.render(in: rendererContext.cgContext)
}.cgImage
- 使用混合圖層模擬
mask效果,比如在要添加圓角的視圖上再疊加一個(gè)部分透明的視圖,只對(duì)圓角部分進(jìn)行遮擋,來達(dá)到mask的效果,不會(huì)離屏渲染,并且可以指定任意角為圓角,推薦使用
// 繪制圓形
- (UIImage *)drawCircleRadius:(float)radius viewSize:(CGSize)viewSize fillColor:(UIColor *)fillColor {
UIGraphicsBeginImageContextWithOptions(viewSize, false, [UIScreen mainScreen].scale);
// 1、獲取當(dāng)前上下文
CGContextRef contextRef = UIGraphicsGetCurrentContext();
//2.描述路徑
// ArcCenter:中心點(diǎn) radius:半徑 startAngle起始角度 endAngle結(jié)束角度 clockwise:是否順時(shí)針
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(viewSize.width * 0.5, viewSize.height * 0.5) radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:NO];
// [bezierPath closePath];
// 3.外邊
[bezierPath moveToPoint:CGPointMake(0, 0)];
[bezierPath addLineToPoint:CGPointMake(viewSize.width, 0)];
[bezierPath addLineToPoint:CGPointMake(viewSize.width, viewSize.height)];
[bezierPath addLineToPoint:CGPointMake(0, viewSize.height)];
[bezierPath closePath];
//4.設(shè)置顏色
[fillColor setFill];
[bezierPath fill];
CGContextDrawPath(contextRef, kCGPathStroke);
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resultImage;
}
最后,我們來看下常見的觸發(fā)離屏渲染都有哪些情景。
1.使用layer.mask,iOS8以上可以使用maskView屬性。mask沒辦法避免離屏渲染。解決方式上面已經(jīng)羅列了。
2.抗鋸齒,可以設(shè)置 allowsEdgeAntialiasing = NO(默認(rèn)就是NO),這就是為什么我們?cè)谟螒蚶锶绻?code>FPS太低的情況會(huì)建議關(guān)閉抗鋸齒的原因。(但是我自己在測(cè)試時(shí)開啟了抗鋸齒沒看到離屏渲染的黃色特征,可能已經(jīng)被蘋果優(yōu)化了。)
3.使用layer.masksToBounds(view.clipsToBounds)+layer.cornerRadius > 0去裁減。iOS9以后UIImageView使用這種方式不會(huì)再離屏渲染了,綜合性能上也很不錯(cuò)。
4.設(shè)置了組透明度(allowsGroupOpacity )開啟,并且當(dāng)視圖透明度(layer.opacity )小于1時(shí),有子視圖或者背景圖的情況會(huì)導(dǎo)致離屏渲染。這個(gè)屬性為YES會(huì)導(dǎo)致視圖里包含的所有其他子視圖的透明度也跟隨父視圖的透明度,子視圖的透明度上限為父視圖的透明度,iOS7之后蘋果默認(rèn)幫我們開啟了這個(gè)屬性,可以通過allowsGroupOpacity = NO關(guān)閉,自己根據(jù)需要去設(shè)置單個(gè)圖層透明度。
然而在 TableView這樣的視圖里設(shè)置 cell 或 cell.contentView 的alpha屬性小于1并不能檢測(cè)離屏渲染的黃色特征,性能上也沒有明顯差別。經(jīng)過摸索發(fā)現(xiàn):只有設(shè)置 tableView 的alpha小于1時(shí)才會(huì)觸發(fā)離屏渲染,對(duì)性能無明顯影響;設(shè)置 cell 的alpha屬性并不會(huì)對(duì)整體的透明度產(chǎn)生影響,只有設(shè)置 cell.contentView 才有效。
5.添加了陰影layer.shadow會(huì)離屏渲染。原因在于雖然layer本身是一塊矩形區(qū)域,但是陰影默認(rèn)是作用在其中非透明區(qū)域的,而且需要顯示在所有layer內(nèi)容的下方,因此根據(jù)畫家算法必須被渲染在先。但矛盾在于此時(shí)陰影的本體(layer和其子layer)都還沒有被組合到一起,怎么可能在第一步就畫出只有完成最后一步之后才能知道的形狀呢?這樣一來又只能另外申請(qǐng)一塊內(nèi)存,把本體內(nèi)容都先畫好,再根據(jù)渲染結(jié)果的形狀,添加陰影到frame buffer,最后把內(nèi)容畫上去(這只是我的猜測(cè),實(shí)際情況可能更復(fù)雜)。不過我們可以通過shadowPath屬性預(yù)先告訴CoreAnimation陰影的幾何形狀,那么陰影就可以先被獨(dú)立渲染出來,不需要依賴layer本體,也就不再需要離屏渲染了。在原來陰影寫法上設(shè)置添加陰影路徑的方法解決離屏渲染layer.shadowPath = [UIBezierPath bezierPathWithRect:view.bounds].CGPath;。
6.采用了光柵化的layer,layer光柵化會(huì)開啟離屏渲染,而主動(dòng)開啟的目的是將離屏渲染的結(jié)果緩存,那么屏幕下一幀渲染就可以復(fù)用這個(gè)成果,避免屏幕刷新頻繁觸發(fā)離屏渲染。處理的layer包括子layer必須是靜態(tài)的,如果layer會(huì)resize、動(dòng)畫,或者在tableView、collectionView中,由于滾動(dòng)的每一幀變化都會(huì)觸發(fā)每個(gè)cell的重新繪制,因此一旦存在離屏渲染,對(duì)性能沖擊將是極大的。
7.使用系統(tǒng)的高斯模糊UIVisualEffect會(huì)離屏渲染,替換使用CIGaussianBlur實(shí)現(xiàn)模糊效果。示例如下
func blurEffect(blurLevel: CGFloat) -> UIImage{
let context = CIContext(options: nil)
let ciImage = CIImage(cgImage: self.cgImage!)
let filter = CIFilter(name: "CIGaussianBlur")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(blurLevel, forKey: "inputRadius")
let result = filter?.value(forKey: kCIOutputImageKey) as! CIImage
let outImage = context.createCGImage(result, from: ciImage.extent)
let blurImage = UIImage(cgImage: outImage!)
return blurImage
}
