iOS 离屏渲染认知及解决方案

661 阅读5分钟

离屏渲染

要理解离屏渲染,需要先了解两个名词,OffScreen Buffer(离屏缓冲区),Frame Buffer(帧缓冲区)。

  • 一般情况下,系统把需要显示位图的直接放到Frame Buffer,然后显示出来。 一般情况下显示流程
  • 当出现离屏渲染之后,会出现一个一个新的缓冲区OffScreen Buffer,在OffScreen Buffer完成在Frame Buffer完成不了的操作,然后提交到帧缓冲区,最后显示出来 当出现离屏渲染的时候的流程

为什么离屏渲染会产生性能问题?

  1. 产生了离屏渲染,会开额外的内存空间OffScreen Buffer
  2. OffScreen Buffer进行渲染然后提交到Frame Buffer需要时间

  • OffScreen Buffer空间是有大小限制的,屏幕像素的2.5倍

产生的原因

可能大家都知道设置圆角就有可能会产生离屏渲染,可是为什么设置圆角就有可能会产生离屏渲染呢?我们结合下面两张图来理解: Apple关于核心动画,给的一个图层介绍 我们在显示一个图层的时候其实包括了三部分内容,backgroundColor,contents,borderWith/borderColor


Apple对cornerRadius的解释 Apple对cornerRadius的解释 我们可以看到,我们通过cornerRadius设置圆角的时候,其实只是对background起作用,如果contents也需要设置圆角,需要设置maskToBounds属性。 接下来在代码中来看一下这个问题 两张ImageView设置圆角,一个出现了离屏渲染,一个没有 看下代码

    // 设置了backgroundColor
    UIImageView * imageOne = [[UIImageView alloc] init];
    imageOne.frame = CGRectMake(100, 180, 100, 100);
    imageOne.image = [UIImage imageNamed:@"photo"];
    imageOne.backgroundColor = [UIColor greenColor];
    imageOne.layer.cornerRadius = 50.f;
    imageOne.layer.masksToBounds = YES;
    [self.view addSubview:imageOne];
    
    //没有设置backgroundColor
    UIImageView * imageTwo = [[UIImageView alloc] init];
    imageTwo.frame = CGRectMake(100, 320, 100, 100);
    imageTwo.image = [UIImage imageNamed:@"photo"];
    imageTwo.layer.cornerRadius = 50.f;
    imageTwo.layer.masksToBounds = YES;
    [self.view addSubview:imageTwo];

当我们只设置了cornerRadiusmaskToBounds并没有发生离屏渲染,但当我们同时设置了background这时就会产生离屏渲染。

根本原因就是我们在没有设置backgroundColor的时候,默认是不需要对background层进行处理,在imageView里面我们只需要渲染出content,然后进行切maskToBounds,放入Frame Buffer能然后就能直接显示出来了。一旦我们设置了background,我们需要对background进行渲染,对contents进行渲染,然后进行混合运算后做圆角处理,再提交到Frame Buffer,进行显示。因为在同一layer下出现了多个图层,圆角的剪切是需要进行混合运算再剪切,通过上面的图例我们知道ImageView其实是有background, contentsmaskToBounds其实是基于cornerRadius来做出运算后对contents切圆角,当我们没有设置backgroundColor也就没有必要去渲染background,对contents基于cornerRadius运算后得出圆角提交帧缓冲区(Frame Buffer)就行了。这也默认是对内存的一种优化,只有当遇到同一Layer下多层图层,直接提交Screen Buffer处理不了的融合显示,才会产生离屏渲染,开辟OffScreen Buffer,在OffScreen Buffer进行相应的各种算法。

哪些情况会触发离屏渲染

  1. layer使用了mask(遮罩)。
  2. layer使用了masksToBoundsclipsToBounds
  3. layer添加了投影。
  4. 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)。
  5. 设置了不为1的组透明度。 因为透明度不为1,像素点颜色的混合运算,需要等这几个不同透明度的图层都渲染好了,提交到Screen Buffer再进行运算,最后提交Frame Buffer进行显示。
  6. 采用了光栅化layer.shouldRasterize

shouldRasterize 的使用建议:

  • 如果layer不能被复用,则没必要开启光栅化。
  • 如果layer不是静态的,需要被频繁修改,比如处于动画之中,开启离屏渲染,会影响效率。
  • 离屏渲染缓存内容有时间限制,如果缓存内容100ms没有被使用,那么就会被抛弃,无法进行复用。
  • 离屏渲染空间有限,屏幕像素的2.5倍,超过了这个大小,无法被缓存

如何解决

官方对离屏渲染产生性能问题也进行了优化: iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染。 iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。

不对layer使用maskToBounds/clipsToBounds,例如imageView

  imageView.clipsToBounds = YES;
  imageView.layer.cornerRadius = 50.f;

使用贝塞尔曲线(UIBezierPath)绘制圆图

[[UIBezierPath bezierPathWithRoundedRect:imageView.boundscornerRadius:imageView.frame.size.width]addClip];

[imageView drawRect:imageView.bounds];

imageView.image=UIGraphicsGetImageFromCurrentImageContext();

//结束画图

UIGraphicsEndImageContext();

[self.view addSubview:imageView];

使用CAShapeLayerUIBezierPath设置圆角

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];

imageView.image = [UIImage imageNamed:@"myImg"];

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];

CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];

//设置大小

maskLayer.frame = imageView.bounds;

//设置图形样子

maskLayer.path = maskPath.CGPath;

imageView.layer.mask = maskLayer;

[self.view addSubview:imageView];

我们可以知道CAShapeLayerCALayer的子类,可以使用CALayer的所有属性,但是只有配合UIBezierPath才会起作用,使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在viewdrawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形,CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于viewdrawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。


对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。

imageView.layer.shadowColor=[UIColorgrayColor].CGColor;

imageView.layer.shadowOpacity=1.0;

imageView.layer.shadowRadius=2.0;

UIBezierPath *path=[UIBezierPathbezierPathWithRect:imageView.frame];

imageView.layer.shadowPath=path.CGPath;

YYImage开源库,对imageView的圆角也可以研究一下。

离屏渲染真的一无是处么?

离屏渲染 离屏渲染并不是一无是处的,虽然会造成很多额外的开销,但也是为了充分利用设备的资源来保证界面的流畅。发生离屏渲染时,是为了引起开发者对性能的关注,减少不必要的透明视图层级。

怎么查看当前显示是否有发生离屏渲染

Simulator菜单Debug模式中打开Color Off-Screen Rendered 查看是否产生离屏渲染的方法.png