iOS离屏渲染初步探究

1,254 阅读4分钟

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