OpenGL-06-离屏渲染原理及触发条件

653 阅读6分钟

一、了解离屏渲染

1、正常渲染流程

APP -----> FrameBuffer(帧缓冲区) -----> Display

  • APP中的数据经过CPU和GPU计算渲染后,把结果放入帧缓冲区,再由视频控制器从甄嬛从去中读取并显示
  • GPU渲染过程中,显示到屏幕上的图像会遵循“画家算法”由远到近的顺序,依次将结果存储到帧缓冲区
  • 视频控制器从帧缓冲区读取一帧的数据将其显示后,就立刻丢弃了这一帧数据,然后进行下一帧的渲染显示。这样做的好处是节省了空间。

2、离屏渲染流程及具体逻辑

APP -----> OffScreenBuffer(离屏缓冲区) -----> FrameBuffer(帧缓冲区) -----> Display

  • 当APP要进行额外的渲染和合并时(比如设置了圆角+裁剪),我们需要把不同的图层进行裁剪+合并的操作,这时就不能直接放入FrameBuffer了,我们要把渲染好的结果放入OffScreenBuffer,等待合适的机会将几个图层进行裁剪、合并叠加的操作,完成后把结果放入FrameBuffer中,由视频控制器读取显示
  • 离屏缓冲区相当于一个临时缓冲区,存放需要进行操作的数据,并不直接使用数据。因此,在方便我们的同时也有缺点,因为是额外开辟的空间,并且还需要转存数据到FrameBuffer中,所以大量的离屏渲染会影响性能,开销较大,也可能造成掉帧
  • OffScreenBuffer空间也是有限制的,是屏幕像素的2.5倍。如果缓存内容并100ms未被使用,会直接丢弃。

二、离屏渲染触发的条件

我们通过代码调试来验证一下,通过打开模拟器的离屏选项来观察

1、高斯模糊 UIBlurEffectView(必定触发)

    //Button 背景色
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 50, 100, 100);
    btn1.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn1];
    
    
    //Button 背景色+高斯模糊
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
    btn2.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn2];
    
    UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *effectVIew = [[UIVisualEffectView alloc]initWithEffect:effect];
    effectVIew.frame = btn2.bounds;
    [btn2 addSubview:effectVIew];

那么我们来看一下高斯模糊的离屏渲染逻辑

  • Content : 渲染内容
  • Capture Content : 捕获内容
  • Horizontal Blur : 水平模糊
  • Vertical Blur :垂直模糊
  • Compositing Pass : 合成过程

2、光栅化(必定触发)

    //Button 背景色
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 50, 100, 100);
    btn1.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn1];
    
    
    //Button 背景色+光栅化
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
    btn2.backgroundColor = [UIColor redColor];
    btn2.layer.shouldRasterize = YES;
    [self.view addSubview:btn2];

开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。 而保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化。

使用光栅化shouldRasterize的一些建议: 1、如果layer不能被复用,没必要打开光栅化 2、如果layer是动态的,需要频繁修改,打开光栅化会造成很大的负荷,不建议打开 3、离屏缓冲区内容有时间限制,超过100ms没有被使用会被丢弃,无法复用 4、离屏缓冲区空间大小有限制,超过屏幕2.5倍就会失效,无法复用

3、阴影

    //Button 背景色
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 50, 100, 100);
    btn1.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn1];


    //Button 背景色+阴影
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
    btn2.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn2];
    
    btn2.layer.shadowColor = UIColor.blackColor.CGColor;
    btn2.layer.shadowOffset = CGSizeMake(2, 2);
    btn2.layer.shadowOpacity = 0.9;

不过,阴影存在优化方案,就是指定一下阴影路径,就能解决了

//在上述代码的基础上添加
btn2.layer.shadowPath = [UIBezierPath bezierPathWithRect:btn2.bounds].CGPath;

4、圆角

我们先以UIButton和UIImageView为例,看不同条件下,圆角是否触发离屏渲染

   //针对UIButton的圆角分情况测试
    for (int i = 0; i < 5; i++) {
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
        btn.frame = CGRectMake(50, 150*i + 50, 100, 100);
        btn.layer.cornerRadius = 50;
        btn.clipsToBounds = YES;
        [self.view addSubview:btn];
        
        if (i == 0) {
            
            //背景色+边框+图片
            btn.backgroundColor = [UIColor redColor];
            btn.layer.borderWidth = 2;
            [btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
            
        }else if (i == 1){
            
            //背景色+边框
            btn.backgroundColor = [UIColor redColor];
            btn.layer.borderWidth = 2;
            
        }else if (i == 2){
            
            //背景色+图片
            btn.backgroundColor = [UIColor redColor];
            [btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
            
        }else if (i == 3){
            
            //边框+图片
            btn.layer.borderWidth = 2;
            [btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
            
        }else if (i == 4){
            
            //图片
            [btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
            
        }
    }
    
    
    //针对UIImageView的圆角分情况测试
    for (int j = 0; j < 5; j++) {
        
        UIImageView *img = [[UIImageView alloc]init];
        img.frame = CGRectMake(200, 150*j + 50, 100, 100);
        img.layer.cornerRadius = 50;
        img.layer.masksToBounds = YES;
        [self.view addSubview:img];
         
        if (j == 0) {
            
            //背景色+边框+图片
            img.backgroundColor = [UIColor redColor];
            img.layer.borderWidth = 2;
            img.image = [UIImage imageNamed:@"btn.png"];
            
        }else if (j == 1){
            
            //背景色+边框
            img.layer.borderWidth = 2;
            img.backgroundColor = [UIColor redColor];
            
        }else if (j == 2){
            
            //背景色+图片
            img.backgroundColor = [UIColor redColor];
            img.image = [UIImage imageNamed:@"btn.png"];
            
        }else if (j == 3){
            
            //边框+图片
            img.layer.borderWidth = 2;
            img.image = [UIImage imageNamed:@"btn.png"];
            
        }else if (j == 4){
            
            //图片
            img.image = [UIImage imageNamed:@"btn.png"];
            
        } 
        
    }

通过上图,打开离屏渲染的选项之后,可以看出10种测试,我们都设置了圆角+ clipsToBounds/masksToBounds,为什么有的触发了离屏渲染,有的没有?

首先,我们来结合CALayer的层级关系和cornerRadius的官方介绍分析一下:

  • CALayer由backgroundColor(背景颜色层)、contents(内容层)、border(边框属性层)构成。
  • 而cornerRadius的文档中明确说明:设置了cornerRadius,只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。
  • 那么我们看代码中: 1、针对UIButton,只要是 图片+ clipsToBounds(即masksToBounds)的情况,都会触发离屏渲染 2、针对UIImageView,只有 图片+背景色/边框+ masksToBounds,才会触发离屏渲染 【这里我们要看一下iOS官方针对UIImageView做的一些优化: 1、在iOS9之前,UIImageView和UIButton通过cornerRadius+masksToBounds/clipsToBounds设置圆角都会触发离屏渲染, 2、在iOS9以后,针对UIImageView中的image设置圆角并不会触发离屏渲染,如果加上了背景色或者阴影等其他效果还是会触发离屏渲染的】

这样我们就解释的通了。

那么我们这里的contents仅仅指的是图片吗?

其实并不是,于是笔者尝试了以下代码,总结出,contents也可以是有色信息(颜色、图片)的子视图

for (int i = 0; i < 3; i++) {
        
        
       UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
       btn.frame = CGRectMake(50, 150*i + 50, 100, 100);
       btn.backgroundColor = [UIColor redColor];
       btn.layer.cornerRadius = 50;
       btn.clipsToBounds = YES;
       [self.view addSubview:btn];
       
       if (i == 0) {
           //无颜色
           UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
           btn1.frame = CGRectMake(0, 0 , 50, 50);
           btn1.backgroundColor = [UIColor clearColor];
           [btn addSubview:btn1];
           
       }else if (i == 1){
           //有颜色
           UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
           btn1.frame = CGRectMake(0, 0 , 50, 50);
           btn1.backgroundColor = [UIColor blackColor];
           [btn addSubview:btn1];
           
       }else if (i == 2){
           //有图片
           UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
           btn1.frame = CGRectMake(0, 0 , 50, 50);
           [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
           [btn addSubview:btn1];
           
       }

所以,针对圆角如何避免触发离屏渲染,我们可以根据上述条件,根据自身项目需求进行特殊定制

5、layer.mask (遮罩/蒙版)

我们来看一下mask的渲染逻辑 如图:

  • 系统先计算好mask部分,然后保存到离屏缓冲区
  • 计算layer部分,计算好之后保存到离屏缓冲区
  • 对mask和layer进行合并剪裁计算,最后结果提交到FrameBuffer,展示到屏幕上

所以说: mask是覆盖在所有layer及其子layer之上的,可能还带有一定的透明度。 mask也是需要等整个layer树绘制完成,再加上mask和组合后的lzyer进行组合,所以需要开辟一个独立于FrameBuffer的内存,用于将layer及其子layer画完,最后再和mask进行组合,存储到FrameBuffer,视频控制器从FrameBuffer中读取数据显示到屏幕上

优化方案:不使用mask,使用混合图层,在layer上方叠加相应mask形状的半透明layer

6、组透明度(layer.allowsGroupOpacity / layer.opacity)

1、groupOpacity中alpha并不是分别应用到每一层之上,需要整个layer树画完之后,在统一加上alpha,和底层其他layer的像素进行组合,此时显然无法通过一次遍历就得到结果 2、需要另外开启一个独立内存,先将layer及其子layer画好,最后给组合后的图层加上alpha进行渲染,将最终结果存储到帧缓冲区 3、GroupOpacity 开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。

另外,两个半透明的view,通过addSubView方法叠加,也会产生离屏渲染。

优化方案:关闭allowsGroupOpacity属性,根据产品需求自己控制layer透明度

总结

常见的触发情况 1、使用了 mask 的 layer (layer.mask) 2、需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds) ,同时拥有多层layer需要处理的情况 3、设置了组透明度为 YES,并且透明度不为 1 的 layer(layer.allowsGroupOpacity/layer.opacity) 4、添加了阴影 (layer.shadow) 5、采用了光栅化 (layer.shouldRasterize) 6、绘制了文字的 layer (UILabel, CATextLayer, Core Text 等) 7、使用了高斯模糊 8、使用了抗锯齿(edge antialiasing)【allowsEdgeAntialiasing = YES】

三、离屏渲染与性能优化

1、离屏渲染的好处

  • 为了特殊效果,不得不使用。例如系统自动触发的情况:圆角、阴影、高斯模糊、光栅化
  • 提升效率。如果一个效果需要多次用到,我们可以提前渲染保存在offscreenbuffer中,免去重复计算的时间,达到复用的目的。这需要手动触发

2、 如何避免离屏渲染做到性能优化

  • 圆角:虽然并不是所有的圆角+裁剪都会触发,但是我们也要分情况使用,可以使用切好的圆角图片,或者自己使用贝塞尔曲线进行圆角绘制
  • 透明度:多层级的视图添加,不要设置透明度;不要设置组透明度
  • 光栅化:当不存在短时间内需要反复多次大量复用的layer时,shouldRasterize设置为NO
  • 阴影:增加阴影路径
  • mask:使用混合图层,在layer上方叠加相应mask形状的半透明layer
  • 抗锯齿:不开启 allowsEdgeAntialiasing 属性 (默认为NO)