前言
这篇文章不会讲解离屏渲染的产生原因和原理
主要讲解决办法和测试方法还有如何触发
如果想看离屏渲染原理讲解的请点击这里
离屏渲染与正常渲染
屏幕上最终显示的数据有两种加载流程如下图所示
流程图.png一. 如何触发离屏渲染
下面几种情况会触发离屏渲染:
- 1.使用了
mask
的layer (layer.mask)
- 2.需要进行裁剪的
layer (layer.masksToBounds / view.clipsToBounds)
- 3.设置了组透明度为
YES
,并且透明度不为 1 的layer (layer.allowsGroupOpacity/layer.opacity)
- 4.添加了投影的
layer (layer.shadow*)
- 5.采用了光栅化的
layer (layer.shouldRasterize)
- 6.绘制了文字的
layer (UILabel, CATextLayer, Core Text等)
不过,需要注意的是,重写 drawRect:
方法并不会触发离屏渲染。前文中我们提到过,重写 drawRect:
会将 GPU 中的渲染操作转移到 CPU 中完成,并且需要额外开辟内存空间。但根据苹果工程师的说法,这和标准意义上的离屏渲染并不一样,在 Instrument 中开启 Color offscreen rendered yellow
调试时也会发现这并不会被判断为离屏渲染。
设置圆角触发离屏渲染
苹果官方文档对于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’scontents
property; it applies only to the background color and border of the layer. However, setting themasksToBounds
property totrue
causes the content to be clipped to the rounded corners.
我们发现设置cornerRadius大于0时,只为layer的backgroundColor和border设置圆角;而不会对layer的contents设置圆角,除非同时设置了layer.masksToBounds为true(对应UIView的clipsToBounds属性)。
如果这时,你认为layer.masksToBounds或者clipsToBounds设置为true就会触发离屏渲染,这是不完全正确的。
我们先打开模拟器的离屏渲染颜色标记:
截屏2020-07-08下午4.57.42.png在平常写代码时,比如UIButton设置圆角,当设置好按钮的image、cornerRadius、borderWidth、borderColor等属性后,运行发现并没有实现我们想要的效果
let btn0 = UIButton(type: .custom)
btn0.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
//设置圆角
btn0.layer.cornerRadius = 50
//设置border宽度和颜色
btn0.layer.borderWidth = 2
btn0.layer.borderColor = UIColor.red.cgColor
self.view.addSubview(btn0)
//设置背景图片
btn0.setImage(UIImage(named: "picture"), for: .normal)
btn0.backgroundColor = UIColor.blue
截屏2020-07-08下午6.26.16.png
截屏2020-07-08下午6.26.34.png
下面的代码会发生离屏渲染
let btn0 = UIButton(type: .custom)
btn0.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
//设置圆角
btn0.layer.cornerRadius = 50
//设置border宽度和颜色
btn0.layer.borderWidth = 2
btn0.layer.borderColor = UIColor.red.cgColor
btn0.layer.masksToBounds = true
self.view.addSubview(btn0)
//设置背景图片
btn0.setImage(UIImage(named: "picture"), for: .normal)
截屏2020-07-08下午6.34.07.png
其实不光是图片,我们为视图添加一个有颜色、内容或边框等有图像信息的子视图也会触发离屏渲染。
let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = UIColor.blue
// 设置边框宽度和颜色
view.layer.borderWidth = 2.0
view.layer.borderColor = UIColor.black.cgColor
// 设置圆角
view.layer.cornerRadius = 100.0
// 设置裁剪
view.clipsToBounds = true
let subView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
subView.backgroundColor = UIColor.red
// 设置边框宽度和颜色
subView.layer.borderWidth = 2.0
subView.layer.borderColor = UIColor.black.cgColor
// 设置内容
subView.layer.contents = UIImage(named: "picture")?.cgImage
subView.layer.borderWidth = 2.0
subView.layer.borderColor = UIColor.black.cgColor
view.addSubview(subView)
view.center = self.view.center;
self.view.addSubview(view)
截屏2020-07-08下午6.42.47.png
对于避免离屏渲染的方案
除了尽量减少圆角裁剪的使用,还有什么别的办法可以避免圆角+裁剪引起的离屏渲染吗?
由于刚才我们提到,圆角引起离屏渲染的本质是裁剪的叠加,导致 masksToBounds
对 layer
以及所有 sublayer
进行二次处理。那么我们只要避免使用 masksToBounds
进行二次处理,而是对所有的 sublayer
进行预处理,就可以只进行“画家算法”,用一次叠加就完成绘制。
那么可行的实现方法大概有下面几种:
- 1.【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
- 2.【mask】再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
- 3.【UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
- 4.【CoreGraphics】重写 drawRect:,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题。
部分例子
方案一
_imageView.clipsToBounds=YES;
_imageView.layer.cornerRadius=4.0;
方案二
- (instancetype)imageWithCornerRadius:(CGFloat)cornerRadius size:(CGSize)newSize
{
UIImage *originImage = self;
// 开始裁切圆角
CGRect bounds = CGRectMake(0, 0, newSize.width, newSize.height);
UIGraphicsBeginImageContextWithOptions(newSize, NO, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bounds
cornerRadius:cornerRadius];
CGContextAddPath(context, path.CGPath);
CGContextClip(context);
[originImage drawInRect:bounds];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
方案三
//mask遮罩
+ (UIImage*)maskWithShowImage:(UIImage *)sImage maskImage:(UIImage*)mImage{
CGSize size = sImage.size;
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
[sImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
[mImage drawInRect:CGRectMake(0, 0, size.width, size.width) blendMode:kCGBlendModeDestinationIn alpha:1];
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resultImage;
}
方案四
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;
}
网友评论