美文网首页
iOS 离屏渲染

iOS 离屏渲染

作者: 东篱采桑人 | 来源:发表于2020-07-08 15:30 被阅读0次

    离屏渲染,作为一个高频的iOS面试题,曾多次困扰到我。最近在对图形学的接触中,又碰到了,索性今天来次深入剖析,彻底掌握其原理。

    一、离屏渲染的定义

    一般的图像渲染流程中,Core Animation将界面内容分解成一个个图层并存入图层树,GPU遵循”画家算法“由远即近来对图层树中的每一个图层进行渲染,并将渲染结果依次存入帧缓存区Frame Buffer)中。

    这个过程中,每一层被渲染完并提交到帧缓存区后,图层数据会被清空,所以就需要保证单次遍历就能完成每一个图层的渲染。但是在有些场景中,由于图层渲染的有序性,一些图层不能被一次性渲染完,后面还需要再回过头来继续渲染,这时候就不能直接将这一层的渲染结果存入帧缓存区,需要另外开辟一块内存空间进行存放,等图层完全渲染后,再将渲染结果存入帧缓存区中。这块新开辟的内存,称为离屏缓存区Offscreen Buffer)。

    在渲染过程中开辟了离屏缓存区的图形渲染,称为离屏渲染

    二、触发离屏渲染的场景

    离屏渲染产生的原因,是因在渲染过程中需要开辟离屏缓存区,来暂存渲染后的图层。iOS中能触发离屏渲染的场景主要有以下几个:

    1. cornerRadius + masksToBounds

    开发中经常使用cornerRadiusmasksToBounds这两个方法来设置layer的圆角和裁剪,先来看下这两个方法的定义:

    • cornerRadius
      绘制图层背景的圆角时使用的半径,当值设置为大于0.0时,会在图层背景上绘制圆角,仅适用于图层的背景颜色和边框,不影响背景图片和子图层。

    • masksToBounds
      当此属性的值为YES时,Core Animation将创建一个隐式剪贴蒙版,该蒙版与图层的边界匹配并包括圆角效果,所有的子图层都会按照这个蒙版进行裁剪。

    要知道,设置cornerRadiusmasksToBounds不一定会触发离屏渲染,那什么情况下会触发呢?这里直接给出结论:设置layer的圆角和裁剪后,如果在渲染过程中layer(包含子layer)会被分解成2个及以上的需要被渲染的图层时,就会触发离屏渲染。

    原因猜想(若有误,请指正):
    GPU会先对layer的每个图层遍历渲染,然后再统一裁剪,所以在每一层渲染完后,需要先放入离屏缓存区暂存。

    接下来通过对CALayer设置圆角和裁剪的不同案例来验证结论。

    1.1 首先通过设置CALayer属性来验证,这里主要设置backgroundColor,边框以及contents属性,下面创建了6个案例来对比分析:

    • A:只设置backGroundColor
    • B:只设置边框
    • C:只设置contents
    • D:同时设置backgroundColor + 边框
    • E:同时设置background + contents
    • F:同时设置边框 + contents
    注:代码太长,只附上主要步骤。且各layer除了设置属性外,还设置了圆角和裁剪。
    
    //A:只设置backGroundColor
    layerA.backgroundColor = [UIColor blueColor].CGColor;
    
    //B:只设置边框
    layerB.borderColor = [UIColor magentaColor].CGColor;
    layerB.borderWidth = 2.f;
    
    //C:只设置contents
    layerC.contents = (__bridge id _Nullable)([UIImage imageNamed:@"screen"].CGImage);
    
    //D:同时设置backgroundColor + 边框
    layerD.backgroundColor = [UIColor blueColor].CGColor;
    layerD.borderColor = [UIColor magentaColor].CGColor;
    layerD.borderWidth = 2.f;
    
    //E:同时设置background + contents
    layerE.backgroundColor = [UIColor blueColor].CGColor;
    layerE.contents = (__bridge id _Nullable)([UIImage imageNamed:@"screen"].CGImage);
    
    //F:同时设置边框 + contents
    layerF.borderColor = [UIColor magentaColor].CGColor;
    layerF.borderWidth = 2.f;
    layerF.contents = (__bridge id _Nullable)([UIImage imageNamed:@"screen"].CGImage);
    

    打开模拟器的离屏渲染调试(Debug --> Color Off-Screen Rendered),看到结果如下图(黄色表示已触发离屏渲染):

    从结果可以看到,上述6个案例中,只有E、F触发了离屏渲染,其余4个均未触发。分析原因:

    A、B、C案例:单独设置backgroundColor、边框或contents后,layer只被分解生成了一个需要渲染的图层(backgroundColor/边框 对应于背景图层,contents对应于内容图层),在图层被渲染完后不需要暂存,可以直接裁剪,所以不触发离屏渲染。

    D、E、F案例:因为backgroundColor和边框都是作用于背景图层的,所以D案例只生成一个背景图层,不需要暂存,所以不触发。而同时设置了backgroundColor和contents,或同时设置边框和contents,会生成两个需要被渲染的图层--背景图层和内容图层,这时候就需要暂存到离屏缓存区,等待后面统一裁剪。

    1.2 通过添加子layer来验证,这里也创建了三个案例对比验证:

    • G:只添加一个子layer
    • H:添加两个子layer
    • I:设置backgroundColor + 添加一个子layer
    注:代码太长,只附上主要步骤。父layer设置了圆角和裁剪,子layer没有进行代码外的设置。
    
    //G:只添加一个子layer
    subG.backgroundColor = [UIColor  purpleColor].CGColor;
    [layerG addSublayer:subG];
    
    //H:添加两个子layer
    subH1.backgroundColor = [UIColor  purpleColor].CGColor;
    [layerH addSublayer:subH1];
    subH2.backgroundColor = [UIColor  magentaColor].CGColor;
    [layerH addSublayer:subH2];
    
    //I:设置一个属性 + 添加一个子layer
    layerI.backgroundColor = [UIColor blueColor].CGColor;
    subI.backgroundColor = [UIColor  purpleColor].CGColor;
    [layerI addSublayer:subI];
    

    结果如下图所示:

    从结果得知,只有G案例没触发离屏渲染,原因和之前一样,G案例由于父layer没有进行任何可视化设置,因此只生成了一个子layer的背景图层,不需要暂存,所以没触发。H案例两个子layer的背景图层都需要渲染,需要暂存,I案例父layer和子layer的背景图层都需要渲染,也需要暂存,所以H案例和I案例都触发了离屏渲染。

    • Shadow

    先创建三个案例来展示给layer设置阴影的效果:

    • A:只设置backGroundColor
    • B:只设置边框
    • C:添加一个子layer
    注:代码太长,只附上主要步骤,layer的shadowColor设置为黑色。
    
    //A:只设置backgroundColor
    layerA.backgroundColor = [UIColor blueColor].CGColor;
    
    //B:只设置边框
    layerB.borderColor = [UIColor magentaColor].CGColor;
    layerB.borderWidth = 2.f;
    
    //I:添加一个子layer
    subLayer.backgroundColor = [UIColor  purpleColor].CGColor;
    [layerC addSublayer:subLayer];
    

    阴影效果如下:

    从案例效果可得知,阴影是作用在layer(包括子layer)所有内容的”非透明区域“。打开模拟器的离屏渲染调试,可以看到这三个案例都触发了离屏渲染:

    因为阴影是在layer所有子图层的最下方,根据画家算法最先被渲染,但阴影效果是作用于layer的非透明区域,所以需要先暂存,等渲染完所有子图层后,再回过头来绘制阴影效果。

    • group opacity

    组透明, 当layer(包含子layer)被分解生成的所有渲染图层中存在叠加的情况,此时设置layer的opacity值在(0, 1)之间,就会触发离屏渲染。这里也创建4个案例进行验证:

    • A:设置layer的边框和contents
    • B:设置layer的边框,再添加一个有背景色的子layer
    • C:在layer添加两个有部分区域重合的子layer
    • D:在layer上添加两个不重合的的子layer
    注:代码太长,只附上主要步骤,layer的opacity设置为0.5。
    
    //A:设置layer的边框和contents
    layerA.borderColor = [UIColor magentaColor].CGColor;
    layerA.borderWidth = 2.f;
    layerA.contents = (__bridge id _Nullable)([UIImage imageNamed:@"screen"].CGImage);
    
    //B:设置父layer的边框,添加一个有背景色的子layer
    layerB.borderColor = [UIColor magentaColor].CGColor;
    layerB.borderWidth = 2.f;
    subB.backgroundColor = [UIColor blueColor].CGColor;
    [layerB addSublayer:subB];
    
    //C:添加两个有部分区域重合的子layer
    subC1.backgroundColor = [UIColor  blueColor].CGColor;
    [layerC addSublayer:subC1];
    subC2.backgroundColor = [UIColor  redColor].CGColor;
    [layerC addSublayer:subC2];
    
    //D:添加两个不重合的的子layer
    subD1.backgroundColor = [UIColor  blueColor].CGColor;
    [layerD addSublayer:subD1];
    subD2.backgroundColor = [UIColor  redColor].CGColor;
    [layerD addSublayer:subD2];
    

    离屏渲染结果如下图所示:

    从结果可知,只有D案例没有触发离屏渲染,因为A、B、C三个案例都存在渲染图层的叠加,A是背景图层和内容图层叠加,B是背景图层和子layer的图层叠加,C是两个子layer图层的叠加。

    这是因为在一般情况下,设置了layer的opacity后,透明度会作用于每一个图层,在渲染过程中会遍历完成每一层的透明度设置。但当打开了layer的组透明后,如果当layer的各图层中存在图层叠加的情况,渲染过程中会先将所有图层渲染完后,再整体设置透明度,这就需要暂存,所以会触发离屏渲染。从iOS7开始,group opacity会默认打开,可以通过设置allowsGroupOpacity = NO来手动关闭,可以看看对于C案例打开

    • mask

    遮罩,mask是应用在layer和其所有子layer的组合之上的,触发离屏渲染的原因也是一样,需要先渲染完所有图层,再进行处理,所以需要暂存。

    • UIBlurEffect

    毛玻璃效果,同样无法通过一次遍历完成渲染,他需要多次对content进行处理,需要暂存,因此会触发离屏渲染。

    • shouldRasterize

    由于离屏缓存区在渲染过程中具有暂存功能,所以除上述特定场景外,如果有些开发场景需要主动使用,比如想让某个layer的渲染结果可以在下一帧继续被使用,则可以通过设置layer的shouldRasterize = YES来触发。shouldRasterize是Apple提供给开发者用来主动触发离屏渲染的属性,在使用中需要注意以下几点:

    1. 如果layer不需要被复用,就没必要打开光栅化。
    2. 如果layer不是静态的,需要被频繁修改,比如处于动画之中,也没必要打开,因为这样layer的渲染结果不能被复用,每一帧都要重新渲染。
    3. 离屏渲染缓存区内容有时间限制,在100ms时间内如果没被使用,就会被丢弃。
    4. 离屏渲染缓存空间有限,不能超过屏幕总像素的2.5倍大小,否则会失效。

    三、离屏渲染的优缺点

    • 缺点

      1. 需要额外开辟新的离屏缓存区,占用GPU内存空间。
      2. 需要频繁切换上下文环境,会对性能造成很大影响。在界面内容的渲染中,有的layer可以一次渲染完存入帧缓存区中,有的layer会触发离屏渲染,需要先暂存到离屏缓存区,渲染完后再存入帧缓存区,在这个过程中,就需要频繁切换 frame buffer 和 offscreen buffer 的上下文环境。
    • 优点
      在渲染过程中提供图层暂存功能,可以满足一些特定开发场景。

    四、总结

    在layer的渲染过程中,若无法通过一次遍历完成所有图层的渲染,就会触发离屏渲染。本质是在渲染过程中开辟了一些新的离屏缓存区(Offscreen buffer),来暂时储存这些图层。离屏渲染对性能造成很大开销,开发中应尽量避免,但在有些场景下,如果需要复用layer的渲染结果,可以通过打开layer的shouldRasterize来主动触发。

    参考:
    关于iOS离屏渲染的深入研究

    扩展阅读:
    iOS 图形渲染流程

    相关文章

      网友评论

          本文标题:iOS 离屏渲染

          本文链接:https://www.haomeiwen.com/subject/aktaqktx.html