前言
在我們?nèi)粘i_發(fā)過程中經(jīng)常會(huì)遇到離屏渲染,如果能正確的使用離屏渲染能為我們的App性能帶來很大提升。相反的如果不能正確的利用它,會(huì)為我們的App來的性能損耗。所以,如果能理解什么情況下能觸發(fā)離屏渲染,離屏渲染產(chǎn)生的原因,那么我們就能真正的掌握,并合理的利用它,讓它成為我們開發(fā)過程中利器。
如何檢測(cè)離屏渲染
開啟離屏渲染檢測(cè)后,如下圖如果出現(xiàn)離屏渲染的圖層會(huì)被標(biāo)記為黃色:

下面分別介紹模擬器和真機(jī)開啟離屏渲染測(cè)試的方法:
- 模擬器
在模擬器中選中 Debug -> 選擇 Color Off-screen Rendered
- 真機(jī)
在 XCode 中選擇 Debug -> View Debugging -> Rendering -> Color Offscreen-Rendered Yellow
圓角和離屏渲染
UIImageView 是如何觸發(fā)離屏渲染
之前我一直認(rèn)為同時(shí)設(shè)置 layer.cornerRadius 和 layer.maskToBounds = true 會(huì)觸發(fā)離屏渲染。 但是事實(shí)的是這樣的嗎? 接下來我們通過一個(gè)簡(jiǎn)單的測(cè)試,一步一步的理解如何設(shè)置圓角才會(huì)觸發(fā)離譜渲染。

如圖,我們一個(gè)個(gè)來分析
- 例1
對(duì)應(yīng)上圖第一個(gè),代碼如下:
let imageView = UIImageView()
// 設(shè)置背景顏色
imageView.backgroundColor = .red
imageView.frame = .init(x: 100, y: 100, width: 60, height: 60)
// 設(shè)置圓角
imageView.layer.cornerRadius = 30
// 關(guān)閉 masksToBounds
imageView.layer.masksToBounds = false
view.addSubview(imageView)
同時(shí)設(shè)置背景顏色和圓角不會(huì)觸發(fā)離屏渲染。
- 例2
對(duì)應(yīng)上圖第二個(gè),代碼如下:
let imageView = UIImageView()
imageView.backgroundColor = .red
imageView.frame = .init(x: 100, y: 200, width: 60, height: 60)
imageView.layer.cornerRadius = 30
imageView.layer.masksToBounds = true
view.addSubview(imageView)
同時(shí)設(shè)置背景顏色,圓角,同時(shí)開啟 masksToBounds, 不會(huì)觸發(fā)離屏渲染。
通過上面的2個(gè)例子我們?nèi)菀椎贸鼋Y(jié)論: 同時(shí)設(shè)置圓角 和 masksToBounds = true
并不會(huì)觸發(fā)離屏渲染。
我們接著往下分析
- 例3
對(duì)應(yīng)上圖第三個(gè),代碼如下:
let imageView = UIImageView()
imageView.backgroundColor = .red
imageView.frame = .init(x: 100, y: 300, width: 60, height: 60)
imageView.layer.cornerRadius = 30
imageView.layer.masksToBounds = true
imageView.image = UIImage(named: "avatar.jpg")
view.addSubview(imageView)
我們同時(shí)設(shè)置圓角, masksToBounds 和 image 會(huì)觸發(fā)離屏渲染。到這里我們可能會(huì)猜測(cè),如果同時(shí)設(shè)置了上面三個(gè)屬性就會(huì)產(chǎn)生離屏渲染。但是結(jié)論真的是這樣的嗎,我們接著來看下面一個(gè)例子
-
例4
let imageView = UIImageView() imageView.frame = .init(x: 100, y: 400, width: 60, height: 60) imageView.layer.cornerRadius = 30 imageView.layer.masksToBounds = true imageView.image = UIImage(named: "avatar.jpg") view.addSubview(imageView)
這個(gè)例子對(duì)應(yīng)第四個(gè),這時(shí)你會(huì)發(fā)現(xiàn)這次沒有觸發(fā)離屏渲染。上面第3個(gè)imageview和第4個(gè)imageview顯示的內(nèi)容是一樣的,但是為什么一個(gè)觸發(fā)了離屏渲染而另一個(gè)沒有呢?關(guān)鍵代碼是這一句代碼 imageView.backgroundColor = .red ,很明顯例3比例4就多了這一行代碼,從而產(chǎn)生了不同的結(jié)果。
結(jié)論: 在UIImageView中同時(shí)設(shè)置了 背景顏色, 圓角, masksToBounds 還有 image 才會(huì)觸發(fā)離屏渲染。
觸發(fā)離屏渲染的根本原因
事實(shí)上,真正導(dǎo)致離屏渲染觸發(fā)的原因是一個(gè) UIView 上包含了多個(gè)圖層,并且設(shè)置了 layer.cornerRadis 和 layer.maskToBounds = true, 才會(huì)觸發(fā)離屏渲染。
為什么同時(shí)設(shè)置 layer.cornerRadis 和 layer.maskToBounds = true 才會(huì)觸發(fā)離屏渲染呢? 首先我們來看一下 layer.cornerRadis 的官方文檔說明:
/*
Discussion
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 true causes
the content to be clipped to the rounded corners.
The default value of this property is 0.0.
*/
如果我們單獨(dú)設(shè)置圓角只會(huì)對(duì) background color 和border 生效,如果想讓他的 content 產(chǎn)生圓角效果,需要配合 masksToBounds = true 同時(shí)使用才行。
上面說的 UIView 上包含多個(gè)圖層,才會(huì)觸發(fā)離屏渲染,下面我們列舉一下,除了 UIImageView , 還有哪些情況:
- 為
UIView的layer.content設(shè)置內(nèi)容(這里接收的CGImage, 在OS X 上接收的是NSImage),同時(shí)設(shè)置背景顏色,會(huì)觸發(fā)離屏渲染。 -
UIView上存在1個(gè)或多個(gè)subviews,并且為subview設(shè)置了內(nèi)容,這里的內(nèi)容可以是背景顏色,也可以是圖片。
離屏渲染產(chǎn)生的原因
APP在渲染的時(shí)候大概是使用畫家算法: 繪制的過程首先繪制距離較遠(yuǎn)的場(chǎng)景,然后用繪制距離較近的場(chǎng)景覆蓋較遠(yuǎn)的部分。

在沒有設(shè)置圓角和maskToBounds的情況下進(jìn)行繪制的時(shí)候,一個(gè)圖層被繪制進(jìn)Frame Buffer(幀緩沖區(qū))后,就被丟棄了,接著繪制下一個(gè)圖層。大致過程如下圖:

但是在如果開啟了圓角和maskToBounds的情況下,因?yàn)橐獙?duì)多個(gè)圖層進(jìn)行裁剪的操作,就不能再使用上面的方式了,這時(shí)候會(huì)另外開辟出一片區(qū)域叫作 Offscreen Buffer (離屏緩沖區(qū)),用來保存中間的狀態(tài),最終在 Offscreen Buffer 完成渲染,等待圖層需要被顯示的時(shí)候,然后從 Offscreen Buffer 給到 Frame Buffer 進(jìn)行顯示。

上面就是離屏渲染大概產(chǎn)生的原因了,那么離屏渲染對(duì)APP性能有什么影響呢?
離屏渲染對(duì)APP性能的影響
離屏渲染對(duì)APP的GPU資源消耗是非常大的,主要體現(xiàn)在兩個(gè)方面:
- 創(chuàng)建新緩沖區(qū)
要想進(jìn)行離屏渲染,首先要?jiǎng)?chuàng)建一個(gè)新的緩沖區(qū)。
- 上下文切換
離屏渲染的整個(gè)過程,需要多次切換上下文環(huán)境:先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上有需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕。而上下文環(huán)境的切換是要付出很大代價(jià)的。
雖然離屏渲染對(duì)GUP消耗很大,在離屏渲染區(qū)的渲染結(jié)果是會(huì)被暫時(shí)保留下來的,如果下次渲染的時(shí)候,這個(gè)圖層沒有發(fā)生變化,那么離屏渲染的結(jié)果能夠再次被復(fù)用。
離屏渲染保存時(shí)間大概是 100ms, 如果超過這個(gè)時(shí)間沒有被復(fù)用,就會(huì)被丟棄。
離屏渲染的空間是屏幕大小的2.5倍。
觸發(fā)離屏渲染的方式
觸發(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, Core Text 等)
如何正確的使用離屏渲染?
離屏渲染正確打開方式
- 如果
layer不能被復(fù)用, 則沒有必要打開光柵化。 - 如果
layer需要頻繁的被修改,則沒有必要打開光柵化。
圓角的幾種處理方式
- 方案一

- 方案二

- 方案三

- 方案四

- 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];
}