前言
在我们日常开发过程中经常会遇到离屏渲染,如果能正确的使用离屏渲染能为我们的App性能带来很大提升。相反的如果不能正确的利用它,会为我们的App来的性能损耗。所以,如果能理解什么情况下能触发离屏渲染,离屏渲染产生的原因,那么我们就能真正的掌握,并合理的利用它,让它成为我们开发过程中利器。
如何检测离屏渲染
开启离屏渲染检测后,如下图如果出现离屏渲染的图层会被标记为黄色:
下面分别介绍模拟器和真机开启离屏渲染测试的方法:
- 模拟器
在模拟器中选中 Debug
-> 选择 Color Off-screen Rendered
- 真机
在 XCode
中选择 Debug
-> View Debugging
-> Rendering
-> Color Offscreen-Rendered Yellow
圆角和离屏渲染
UIImageView
是如何触发离屏渲染
之前我一直认为同时设置 layer.cornerRadius
和 layer.maskToBounds = true
会触发离屏渲染。 但是事实的是这样的吗? 接下来我们通过一个简单的测试,一步一步的理解如何设置圆角才会触发离谱渲染。
如图,我们一个个来分析
- 例1
对应上图第一个,代码如下:
let imageView = UIImageView()
// 设置背景颜色
imageView.backgroundColor = .red
imageView.frame = .init(x: 100, y: 100, width: 60, height: 60)
// 设置圆角
imageView.layer.cornerRadius = 30
// 关闭 masksToBounds
imageView.layer.masksToBounds = false
view.addSubview(imageView)
同时设置背景颜色和圆角不会触发离屏渲染。
- 例2
对应上图第二个,代码如下:
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)
同时设置背景颜色,圆角,同时开启 masksToBounds
, 不会触发离屏渲染。
通过上面的2个例子我们容易得出结论: 同时设置圆角
和 masksToBounds = true
并不会触发离屏渲染。
我们接着往下分析
- 例3
对应上图第三个,代码如下:
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)
我们同时设置圆角
, masksToBounds
和 image
会触发离屏渲染。到这里我们可能会猜测,如果同时设置了上面三个属性就会产生离屏渲染。但是结论真的是这样的吗,我们接着来看下面一个例子
-
例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)
这个例子对应第四个,这时你会发现这次没有触发离屏渲染。上面第3个imageview和第4个imageview显示的内容是一样的,但是为什么一个触发了离屏渲染而另一个没有呢?关键代码是这一句代码 imageView.backgroundColor = .red
,很明显例3比例4就多了这一行代码,从而产生了不同的结果。
结论: 在UIImageView
中同时设置了 背景颜色
, 圆角
, masksToBounds
还有 image
才会触发离屏渲染。
触发离屏渲染的根本原因
事实上,真正导致离屏渲染触发的原因是一个 UIView
上包含了多个图层,并且设置了 layer.cornerRadis
和 layer.maskToBounds = true
, 才会触发离屏渲染。
为什么同时设置 layer.cornerRadis
和 layer.maskToBounds = true
才会触发离屏渲染呢? 首先我们来看一下 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.
*/
如果我们单独设置圆角只会对 background color
和border
生效,如果想让他的 content
产生圆角效果,需要配合 masksToBounds = true
同时使用才行。
上面说的 UIView
上包含多个图层,才会触发离屏渲染,下面我们列举一下,除了 UIImageView
, 还有哪些情况:
- 为
UIView
的layer.content
设置内容(这里接收的CGImage
, 在OS X 上接收的是NSImage
),同时设置背景颜色,会触发离屏渲染。 -
UIView
上存在1个或多个subviews
,并且为subview
设置了内容,这里的内容可以是背景颜色,也可以是图片。
离屏渲染产生的原因
APP在渲染的时候大概是使用画家算法: 绘制的过程首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分。
在没有设置圆角和maskToBounds的情况下进行绘制的时候,一个图层被绘制进Frame Buffer
(帧缓冲区)后,就被丢弃了,接着绘制下一个图层。大致过程如下图:
但是在如果开启了圆角和maskToBounds的情况下,因为要对多个图层进行裁剪的操作,就不能再使用上面的方式了,这时候会另外开辟出一片区域叫作 Offscreen Buffer
(离屏缓冲区),用来保存中间的状态,最终在 Offscreen Buffer
完成渲染,等待图层需要被显示的时候,然后从 Offscreen Buffer
给到 Frame Buffer
进行显示。
上面就是离屏渲染大概产生的原因了,那么离屏渲染对APP性能有什么影响呢?
离屏渲染对APP性能的影响
离屏渲染对APP的GPU资源消耗是非常大的,主要体现在两个方面:
- 创建新缓冲区
要想进行离屏渲染,首先要创建一个新的缓冲区。
- 上下文切换
离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
虽然离屏渲染对GUP消耗很大,在离屏渲染区的渲染结果是会被暂时保留下来的,如果下次渲染的时候,这个图层没有发生变化,那么离屏渲染的结果能够再次被复用。
离屏渲染保存时间大概是 100ms
, 如果超过这个时间没有被复用,就会被丢弃。
离屏渲染的空间是屏幕大小的2.5倍。
触发离屏渲染的方式
触发离屏渲染的几种常见情况:
- 使用了 mask 的 layer (layer.mask)
- 需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
- 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/ layer.opacity)
- 添加了投影的 layer (layer.shadow*)
- 采用了光栅化的 layer (layer.shouldRasterize)
- 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
如何正确的使用离屏渲染?
离屏渲染正确打开方式
- 如果
layer
不能被复用, 则没有必要打开光栅化。 - 如果
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];
}