Offscreen Rendering
(离屏渲染)概念理解:
离屏渲染 是指系统为了绘制圆角(cornerRadius
)、阴影(shadow*
)、组透明度(allowsGroupOpacity/opacity
)、光栅化(shouldRasterize
)等等一系列效果,从而额外开辟了 离屏缓冲区(Offscreen Buffer
)然后将离屏缓冲区中的纹理图片合成到 帧缓冲区(Frame Buffer
) 最后再进行渲染的行为。
当然,并不是只要存在上述绘制操作就一定会进行离屏渲染。
xocde中如何检测离屏渲染效果:

勾选上Color Offscreen-Rendered Yellow
选项后如果存在离屏渲染的控件就会呈现出黄色的标记:

Offscreen Rendering
(离屏渲染)详细流程
首先我们来看一张流程图:

从这张图我们可以清晰的看出,③ 这张图是由 ① 和 ② 合成的。① 和 ② 存在于 离屏缓冲区(Offscreen Buffer
),③存在于 帧缓冲区(Frame Buffer
)。
- 那么问题来了为什么会产生 离屏缓冲区(
Offscreen Buffer
) 呢?
这个问题就跟苹果图片渲染的机制有关系了,因为苹果渲染图片采用的是 双缓存机制 ,也就是内存中只开辟了两个 帧缓冲区(Frame Buffer
)。 所以,需要绘制多个图层合成图片时,系统并不会直接将渲染好的图层放到 帧缓冲区(Frame Buffer
),而是将每一个渲染好的图层暂时存储到 离屏缓冲区(Offscreen Buffer
) 中,等待所有关联的图层全都渲染完毕后统一在帧缓冲区合成。
下面我们来了解一下系统自带的“毛玻璃效果”渲染流程,也是离屏渲染

Render Content
(渲染内容) -> Capture Content
(捕获内容) -> Horizontal Blur
(水平模糊) -> Vertical Blur
(垂直模糊) -> Compositing Pass
(合成过程)
其中Capture Content
(捕获内容) 获取的是压缩后的图片,并不是原图。
shouldRasterize
光栅化使用建议:
- 如果
layer
不能被复用,则没有必要打开光栅化。 - 如果
layer
不是静态的,经常被修改,比如处于动画之中,那么设置光栅化反而影响效率。
cornerRadius、layer.masksToBounds / view.clipsToBounds
设置layer. cornerRadius
只会设置 backgroundColor
和 border
的圆角,不会设置 content
的圆角。除非设置了 layer.masksToBounds
为 true
或设置 view.clipsToBounds
为 true
。
所以,单纯设置cornerRadius
并不会造成离屏渲染。
再思考一下,设置了layer. cornerRadius
,再设置 layer.masksToBounds
为 true
或view.clipsToBounds
为 true
就一定会触发离屏渲染吗?下面我们查看一段代码:
//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 设置了图片+背景色; 离屏渲染
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 只设置了图片,无背景色;
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"];
经过测试我们发现btn1
、img1
会产生离屏渲染,btn2
、img2
并不会产生离屏渲染。
需要离屏渲染的控件,在绘制的时候都需要裁剪不止一个图层,当需要裁剪的图层只有一个时是不会产生离屏渲染的。
我们可以结合上面的介绍想想一下,当需要裁剪的图层只有一个时,系统就会直接将渲染好的纹理放到 帧缓冲区(Frame Buffer
),没有必要使用离屏缓冲区(Offscreen Buffer
), 然后然后等待runloop
的到来,通过 垂直同步 技术渲染到 屏幕上。
离屏渲染限制
- 离屏渲染缓存内容有时间限制,缓存内容如果100ms没有被使用,那么它就会被丢弃,无法复用。
- 离屏渲染缓存空间有限制,超过2.5被屏幕像素大小,也会失效,且无法复用。
圆角处理方法:
- 方法一
img.layer.cornerRadius = 50;
img.layer.masksToBounds = YES;
- 方法二
func createRoundedImage(cornerRadius: CGFloat) -> UIImage? {
let w = self.size.width
let h = self.size.height
let scale = UIScreen.main.scale
var cornerR = cornerRadius
if cornerRadius < 0 { return self }
if (cornerR > min(w, h)) {
cornerR = min(w, h) * 0.5
}
let imageFrame = CGRect(x: 0.0, y: 0.0, width: w, height: h)
UIGraphicsBeginImageContextWithOptions(self.size, false, scale)
let bezierPath = UIBezierPath(roundedRect: imageFrame, cornerRadius: cornerR)
bezierPath.addClip()
self.draw(in: imageFrame)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
return self
}
return image
}
- 方法三
在view上添加一个圆形的遮罩图片。
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];
[path stroke];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
常见触发离屏渲染的几种情况:
1、使用 了
mask
的layer
(layer.mask
)
2、需要进行裁剪的layer
(cornerRadius、layer.masksToBounds / view.clipsToBounds
)
3、设置了组透明度为true
,并且透明度不为1的layer
(allowsGroupOpacity/opacity
)
4、添加了投影layer
(show*
)
5、设置了光栅化的layer
(shouldRasterize
)
6、绘制了文字的layer
(UILabel
、CATextLayer
、Core Text
等)
网友评论