作为iOS开发者我们都知道,当设置UIView的圆角以及masksToBounds = YES的时候就会触发离屏渲染,然而实际上真的是这样吗?下面我们通过几个实际例子来研究下离屏渲染。
我们以UIImageView为例子来进行探究
首先我们打开模拟器上离屏渲染的开关,如图所示
圆角设置导致离屏渲染
只设置背景颜色
-(void)imageViewOne {
//只设置背景颜色
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(Width/2 - 50, 120, 100, 100)];
imageView.backgroundColor = [UIColor redColor];
imageView.layer.cornerRadius = imageView.frame.size.width/2;
imageView.layer.masksToBounds = YES;
[self.view addSubview:imageView];
}

如上图所示我们发现,尽管我们设置了圆角以及masksToBounds = YES,但是并没有触发离屏渲染。
只设置图片
-(void)imageViewTwo {
//只设置图片
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(Width/2 - 50, 250, 100, 100)];
imageView.image = [UIImage imageNamed:@"we_chat"];
imageView.layer.cornerRadius = imageView.frame.size.width/2;
imageView.layer.masksToBounds = YES;
[self.view addSubview:imageView];
}

我们发现,当只设置图片时依旧没有触发离屏渲染
设置背景颜色以及图片
-(void)imageViewThree {
//设置图片以及背景颜色
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(Width/2 - 50, 400, 100, 100)];
imageView.image = [UIImage imageNamed:@"we_chat"];
imageView.backgroundColor = [UIColor redColor];
imageView.layer.cornerRadius = imageView.frame.size.width/2;
imageView.layer.masksToBounds = YES;
[self.view addSubview:imageView];
}

此时我们发现,当设置图片、背景颜色时开启了圆角以及设置了masksToBounds = YES触发了离屏渲染(黄色部分)
设置背景图片以及圆边
-(void)imageViewFour {
//设置图片以及圆边
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(Width/2 - 50, 550, 100, 100)];
imageView.image = [UIImage imageNamed:@"we_chat"];
imageView.layer.borderWidth = 1;
imageView.layer.borderColor = [UIColor redColor].CGColor;
imageView.layer.cornerRadius = imageView.frame.size.width/2;
imageView.layer.masksToBounds = YES;
[self.view addSubview:imageView];
}

我们发现当设置图片以及圆边时触发了离屏渲染
设置圆角离屏渲染问题分析及总结
通过以上案例我们分析到当只设置图片或背景颜色的时候,尽管我们设置了圆角以及masksToBounds = YES,但是并没有触发离屏渲染,只有我们同时设置背景颜色、图片或者设置边框(或者添加有颜色、图片、边框等子视图)的时候才触发了离屏渲染。
为什么会这样呢?
实际上图层绘制是遵循着“画家算法”的,即先绘制距离较远的再绘制较近的。因此当view既有颜色又有图片的时候,它会先绘制view的颜色,此时绘制好view的背景颜色会存放在offScreen Buffer中,然后等待绘制图片,当图片绘制完成后继续存放在offScreen Buffer中等待绘制圆角或边框等,最后再将offScreen Buffer中存储的数据合并成我们最终看到的效果。然而正常的情况下我们绘制完画面就应该展示,正因为我们将绘制的数据存放在offScreen Buffer中才导致了离屏渲染。
实际上我们设置图片、颜色或者边框等都是对content进行了赋值,因此masksToBounds = YES才会导致离屏渲染;这样我们就会明白并不是所有设置masksToBounds = YES都会导致离屏渲染,只有对content赋值了(不一定是手动赋值)且设置了masksToBounds = YES才会。
最后总结
通过以上分析,其实只有单图层的时候无论是否设置masksToBounds = YES都不会导致离屏渲染,当多个图层需要单独绘制且设置masksToBounds = YES的时候才会触发离屏渲染。
光栅化导致离屏渲染
使用shouldRasterize光栅化也会导致离屏渲染,因此对于使用光栅化建议:
- 如果layer不能复用,不建议使用光栅化
- 如果layer不是静态的,它需要频繁的被修改,比如处于动画之中,那么就不需要开启光栅化
- 离屏渲染缓存有时间限制约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 等)
解决由于圆角设置导致离屏渲染的方案
使用clipsToBounds = YES
例如:_imageView.clipsToBounds=YES;
_imageView.layer.cornerRadius=4.0;
将图片绘制成带圆角图片再使用

使用贝塞尔曲线绘制圆角

使用带有圆角的透明图片当背景色

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