美文网首页OpenGL & Metal
关于离屏渲染的深入理解

关于离屏渲染的深入理解

作者: 黑眼豆豆_ | 来源:发表于2020-07-08 10:21 被阅读0次

    前言

            离屏渲染,这应该是一个老生常谈的话题了。许多人对于离屏渲染都可以说出一二,而且在面试中,离屏渲染也是一个面试官很爱问的问题,比如说为何会出现离屏渲染,如何防止离屏渲染,可能大家都会说减少圆角,减少边框,减少阴影等等。但是,这是这只是冰山一角。
            那么,我们今天就一起来探讨一下离屏渲染

    原理

    首先我们看一个例子。

        //1.按钮存在背景图片
        UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn1.frame = CGRectMake(100, 30, 100, 100);
        btn1.layer.cornerRadius = 50;
        [self.view addSubview:btn1];
        btn1.layer.shouldRasterize = YES;
        [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
        btn1.layer.masksToBounds = YES;
    
        //2.按钮不存在背景图片
        UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn2.frame = CGRectMake(100, 180, 100, 100);
        btn2.layer.cornerRadius = 50;
        btn2.backgroundColor = [UIColor blueColor];
        [self.view addSubview:btn2];
        btn2.clipsToBounds = YES;
    
        //3.UIImageView 设置了图片+背景色;
        UIImageView *img1 = [[UIImageView alloc]init];
        img1.frame = CGRectMake(100, 320, 100, 100);
        img1.backgroundColor = [UIColor blueColor];
        [self.view addSubview:img1];
        img1.layer.cornerRadius = 50;
        img1.layer.masksToBounds = YES;
        img1.image = [UIImage imageNamed:@"btn.png"];
    
        //4.UIImageView 只设置了图片,无背景色;
        UIImageView *img2 = [[UIImageView alloc]init];
        img2.frame = CGRectMake(100, 480, 100, 100);
        [self.view addSubview:img2];
        img2.layer.cornerRadius = 50;
        img2.layer.masksToBounds = YES;
        img2.image = [UIImage imageNamed:@"btn.png"];
    

    这是我们代码中常常会出现的情况。那么,这几种会造成离屏渲染吗?接下来我们通过调试。运行代码。

    Simulator -> Debug -> Color Off-screen Rendered

    image.png

    接下来可以看到结果。


    image.png

    通过结果可以看到,第一个和第三个图像出现了离屏渲染,别急,待会儿和大家一一解答。

    渲染流程

    渲染流程对比图.png

    通过图片可以看到,和正常的渲染流程不同的是,离屏渲染会把数据放入一个离屏缓冲区(Offscreen Buffer)中,待所有图层的结果进行混合计算完成后才会在屏幕进行展示。


    mask.jpg

    举一个例子,如上图所示,在一个相机的图标上添加一个遮罩(Mask),总共会经过这么几步:

    • 在App提交到Core Animation
    • 再到渲染服务(Render Server),接下来的工作用OpenGL/Metal来进行操作
    • 相机按钮来说,首先利用顶点着色器(Vertex Shader)绘制顶点,然后进行图元装配(Primitive Assembly),最后到片元着色器(Pixel Shader)进行渲染,最后获得的数据存入离屏缓冲区(Offscreen Buffer)中。 Pass 1
    • 遮罩相机的渲染流程类似,渲染完成后存入另一个离屏缓冲区(Offscreen Buffer)中。 Pass 2
    • Pass 1Pass 2中存入到离屏缓冲区(Offscreen Buffer)的数据拿出来进行渲染从而展示在界面中。

    总结,通过以上的流程我们可以看到,Pass 1和Pass 2两次步骤的数据因为要进行合并和渲染,所以在执行完成后并不能进行丢弃,而必须存入离屏缓冲区这个中间变量,所以就要开辟一个空间,而造成离屏渲染
    用一句通俗的话讲,离屏渲染出现的原因就是App进行额外的渲染和合并,会将中间过程产生的数据存入离屏缓冲区(Offscreen Buffer),从而造成离屏渲染。

    离屏渲染的危害

    • 离屏渲染会开辟一个离屏缓冲区(Offscreen Buffer),所以会占用额外的存入空间
    • 离屏缓冲区(Offscreen Buffer)空间的限制是屏幕像素的2.5倍
    • 从离屏缓冲区(Offscreen Buffer)到帧缓冲区(FrameBuffer)这个过程会造成 时间和性能的损耗。
    • 容易掉帧,造成性能问题。

    离屏渲染触发方式

    主动触发 -- 开启光栅化(ShouldRasterize)

    When the value of this property is true, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.

            用一句话概括就是当shouldRasterize设成true时,layer被渲染成一个bitmap,并缓存起来,等下次使用时不会再重新去渲染了。
            
    由此可以得知,光栅化开启时,会造成离屏渲染,但是光栅化会对layer进行复用,所以这就是一个很矛盾的点了,离屏渲染会造成性能损耗,但是光栅化又会节约内存,所以到底怎么用光栅化呢?在这里给大家几个建议:

    • 如果layer不能被复用,没有必要打开光栅化
    • 如果layer不是静态的,会被频繁渲染,开启离屏渲染反而影响效率
    • 离屏渲染内容有时间限制,缓存内容100ms以内没有被使用,那么它就会被丢弃,无法复用
    • 离屏渲染内容有空间限制,超过屏幕2.5倍像素大小,也会失效,无法复用

    被动触发

    设置圆角触发的离屏渲染的离屏渲染。

    cornerRadius.jpg

    将半径设置为大于0.0的值会使该图层开始在其背景上绘制圆角。默认情况下,拐角半径不适用于图层的contents属性中的图像;它仅适用于图层的背景颜色和边框。但是,将masksToBounds属性设置为true会导致内容被裁剪到圆角。

    此属性的默认值为0.0。

    通过官方文档,我们可以知道设置cornerRadius仅适用于图层的背景颜色(backgroundColor)边框(border),而对其中的内容(content)无效,而如果要对内容设置圆角,则需要加上masksToBounds

    clipsToBounds(UIView)是指视图上的子视图,如果超出父视图的部分就截取掉
    masksToBounds(CALayer)却是指视图的图层上的子图层,如果超出父图层的部分就截取掉

    我们来看一个例子。

        UIImageView *img = [[UIImageView alloc]init];
        img.frame = CGRectMake(150, 300, 100, 100);
        img.backgroundColor = [UIColor blueColor];
        [self.view addSubview:img];
        img.layer.cornerRadius = 50;
        img.image = [UIImage imageNamed:@"btn.png"];
    

    看到结果,

    非圆角.jpg
    在上面的例子中我们可以看到,虽然设置了img.layer.cornerRadius = 50,但是仍然没有出现圆角:我们对img对象设置了圆角,但是cornerRadius对图片内容无效,所以就导致image里的内容仍然是正方形,盖在img上面后,出现了非圆角的情况。
    接下里,我们加上
      img.layer.masksToBounds = YES;
    

    可以看到效果


    圆角.jpg

    但是,这个时候会触发离屏渲染。因为我们本身背景色是一个图层,而内容又是一个图层,当两个图层叠加而且需要进行组合处理(画圆角)就会触发离屏渲染。

    还记得上面那个例子吗?

        //1.按钮存在背景图片
        UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn1.frame = CGRectMake(100, 30, 100, 100);
        btn1.layer.cornerRadius = 50;
        [self.view addSubview:btn1];
        btn1.layer.shouldRasterize = YES;
        [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
        btn1.layer.masksToBounds = YES;
    
        //2.按钮不存在背景图片
        UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn2.frame = CGRectMake(100, 180, 100, 100);
        btn2.layer.cornerRadius = 50;
        btn2.backgroundColor = [UIColor blueColor];
        [self.view addSubview:btn2];
        btn2.clipsToBounds = YES;
    
        //3.UIImageView 设置了图片+背景色;
        UIImageView *img1 = [[UIImageView alloc]init];
        img1.frame = CGRectMake(100, 320, 100, 100);
        img1.backgroundColor = [UIColor blueColor];
        [self.view addSubview:img1];
        img1.layer.cornerRadius = 50;
        img1.layer.masksToBounds = YES;
        img1.image = [UIImage imageNamed:@"btn.png"];
    
        //4.UIImageView 只设置了图片,无背景色;
        UIImageView *img2 = [[UIImageView alloc]init];
        img2.frame = CGRectMake(100, 480, 100, 100);
        [self.view addSubview:img2];
        img2.layer.cornerRadius = 50;
        img2.layer.masksToBounds = YES;
        img2.image = [UIImage imageNamed:@"btn.png"];
    
    • 为什么案例1会触发离屏渲染?因为UIButton本身有一个Layer,所以跟Image进行叠加画圆角,会导致离屏渲染。
    • 案例2,给UIButton添加背景色,此时只有一个图层,所以不会导致离屏渲染。
    • 案例3, UIImageView中本身的Layer和Content的Layer进行叠加,所以导致离屏渲染。
    • 案例4,同案例2。

    防止

    圆角

    • 祈求UI小姐姐给切一张图盖在上面,造成圆角的假象,但前提是要和UI小姐姐搞好关系。
    • 使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角,需要注意的是Core Graphics通过CPU重新绘制一份带圆角的视图来实现圆角效果,会大大增加CPU的负担,而且相当于多了一份视图拷贝会增加内存开销。但是就显示性能而言,由于没有触发离屏渲染,所以能保持较高帧率。
    • 使用CAShapeLayer和UIBezierPath设置圆角,通过设置view.layer的mask属性,可以将另一个layer盖在view上,也可以设置圆角,但是mask同样会触发离屏渲染,但是对内存的消耗最少,而且渲染快速
    -(void)pd_setRadius:(float)radius{
        
        UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
        
        CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
        maskLayer.frame = bounds;
        maskLayer.path = maskPath.CGPath;
        
        [self.layer setMask: maskLayer];
    }
    
    • CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值;
    • CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)
    • 使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形
    • CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。
    • 总的来说就是用CAShapeLayer的内存消耗少,渲染速度快,建议使用优化方案2。

    阴影

    设置阴影后,设置CALayer的 shadowPath。

    let shadowView = UIView()
    shadowView.frame = CGRect(x: 50, y: 100, width: 200, height: 200)
    shadowView.layer.backgroundColor = UIColor.green.cgColor
    shadowView.layer.shadowColor = UIColor.black.cgColor
    shadowView.layer.shadowOpacity = 0.5
    shadowView.layer.shadowRadius = 10
    shadowView.layer.shadowOffset = CGSize(width: 10, height: 10)
    shadowView.layer.shadowPath = UIBezierPath(rect: shadowView.bounds).cgPath
    view.addSubview(shadowView)
    

    相关文章

      网友评论

        本文标题:关于离屏渲染的深入理解

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