美文网首页
OpenGL-06-离屏渲染原理及触发条件

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

作者: 宇宙那么大丶 | 来源:发表于2020-07-17 17:13 被阅读0次

    一、了解离屏渲染

    1、正常渲染流程

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

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


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

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

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


      image.png

    二、离屏渲染触发的条件

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


    image.png
    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];
    
    image.png

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


    image.png
    • 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];
    
    image.png

    开启光栅化后,会触发离屏渲染,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;
    
    
    image.png

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

    //在上述代码的基础上添加
    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"];
                
            } 
            
        }
    
    image.png

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

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


    image.png
    image.png
    • 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];
               
           }
    
    
    image.png

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

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

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


    image.png

    如图:

    • 系统先计算好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)

    相关文章

      网友评论

          本文标题:OpenGL-06-离屏渲染原理及触发条件

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