美文网首页
离屏渲染--原理及解决方案

离屏渲染--原理及解决方案

作者: iOSer_jia | 来源:发表于2020-07-07 14:18 被阅读0次

    概述

    离屏渲染(offscreen-rendering)问题是iOS性能优化中需要解决的一个问题,一般来说,少量的离屏渲染其实对我们APP的性能并没有太大影响,但是如果有大量离屏渲染问题,就会导致明显的掉帧和卡顿,离屏渲染就成为了我们APP优化必须解决的一个问题。
    本文将从渲染的角度对离屏渲染产生的原因进行阐述。

    离屏渲染和在屏渲染(onscreen-rendering)

    在理解这这两个概念前,我们先了解iOS渲染的流程。我们通过UIKit设置界面的颜色、位置后,会经由Core Animation,再通过OpenGL ES/Metal绘制图像放到GPU的帧缓冲区中,在下一次垂直信号到来时将帧缓冲区的内容渲染到屏幕上,这就渲染的流程,帧缓冲区的大小也正好等于屏幕像素点的大小。


    渲染流程

    正常来说,GPU不需要额外开辟一个缓冲空间来进行渲染操作,而这种在当前屏幕缓冲区进行的渲染操作称为在屏渲染。但是在某些情况下,GPU需要额外开辟一块缓冲区进行渲染操作,这种情况称为离屏渲染。

    离屏渲染的检测

    模拟器:Debug->Color Offscreen-Rendered (离屏渲染的图层高亮成黄,可能存在性能问题)
    真机:1.Xcode->Debug->View Debugging->Rendering->Color Offscreen-Rendered Yellow
    2.Instrument->Core Animation->Color Offscreen-Rendered Yellow

    离屏渲染产生的原因

    1.系统触发
    通常我们在渲染一个layer的内容时,通常采用油画算法,用先远后近的方式,将一层一层的图层由下至上渲染到屏幕上,正常情况,已经绘制好的内容是可以直接渲染到屏幕上的,并不需要额外的空间保存。但如果一个图层经过着色管道后产生的结果不能马上渲染到屏幕上,而是需要与另一个图层经过管道得到的结果再次进行计算、混合后产生特殊效果才能渲染到屏幕上时,GPU就需要额外开辟一个缓冲区保存这两次中间结果,这个缓冲区就称为离屏缓冲区,这个时候就产生了离屏渲染。
    2.手动触发
    除了系统触发的离屏渲染,我们也可以通过设置layer的shouldRasterize为YES来触发离屏渲染,在某些场景下,打开 shouldRasterize 可以将一个layer反复利用,从而达到提升效率的优势。

    并不是所用的场景下开启光栅化(shouldRasterize)都可以提升效率,如果layer不能被复用,或者layer不是静态,会被频繁修改(比如处于动画之中),那么打开光栅化了反而有可能影响了效率。
    另外,离屏渲染缓存内容有时间和空间限制,缓存内容如果100ms内没有被使用,那么它就会被丢弃,如果离屏缓冲区的大小超过2.5倍屏幕大小的话也会失效,无法进行复用。

    离屏渲染对性能的影响

    相对于在屏渲染,离屏渲染需要开辟一个新的缓冲区,另外离屏渲染在整个过程中需要多次切换上下文环境,如果一帧画面中,存在多个需要的离屏渲染的view,那么GPU的执行时间将会被拉长,如果无法在下一个垂直同步信号到来时完成计算,那么将会产生卡顿,造成性能问题。

    iOS离屏渲染产生的场景

    通常iOS在以下场景是有可能产生离屏渲染的:

    1. layer使用了cornerRadius+masksToBounds设置圆角
    2. layer设置阴影
    3. layer使用了mask
    4. layer设置了组透明度
    5. layer采用了光栅化
    6. 绘制了文字的layer(UILabel,CATextLayer,Core Text等)

    解决layer圆角产生离屏渲染的问题

    1.产生的场景
    并不是每一个使用了cornerRadius+masksToBounds设置圆角的layer的圆角都会产生离屏渲染,如果一个layer只有一个图层,那么通过cornerRadius+masksToBounds设置圆角,是不会产生离屏渲染的,比如下面两个view:

        UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 44, 100, 100)];
        imgView.image = [UIImage imageNamed:@"img"];
        imgView.layer.cornerRadius = 5;
        imgView.clipsToBounds = YES;
    

    或者:

        UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 44, 100, 100)];
        imgView.backgroundColor = UIColor.redColor;
        imgView.layer.cornerRadius = 5;
        imgView.clipsToBounds = YES;
    

    如果一个View只有一个图层,通过cornerRadius+masksToBounds并不会产生离屏渲染,但是如果给例子中的imgView同时设置了背景颜色和图片,或者设置了边框,又或者给imgView添加子View时,就会产生离屏渲染了。

        imgView.image = [UIImage imageNamed:@"img"];
        imgView.backgroundColor = UIColor.redColor;
        imgView.layer.cornerRadius = 5;
        imgView.clipsToBounds = YES;
    
        imgView.image = [UIImage imageNamed:@"img"];
        imgView.layer.borderWidth = 1.f;
        imgView.layer.cornerRadius = 5;
        imgView.clipsToBounds = YES;
    
        imgView.image = [UIImage imageNamed:@"img"];
        imgView.layer.borderWidth = 1.f;
        imgView.layer.cornerRadius = 5;
        imgView.clipsToBounds = YES;
        
        UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(120, 44, 40, 40)];
        subView.backgroundColor = UIColor.blueColor;
        [imgView addSubview:subView];
    

    以上代码均会产生离屏渲染。

    2.产生的原因

    了解这个问题前,我们需要知道,一个layer是由backgroundColor、contents、border三部分组成的

    layer的组成

    而从cornerRadius的官方介绍看,单纯设置cornerRadius只能裁剪backgroud和border部分,contents需要陪着masksToBounds才能设置圆角。所以UIImageView图片内容的裁剪,实际上backgroud或者border的mask与其进行混合后产生的结果。


    cornerRadius官方文档

    如果layer中只有一个layer的contents,或者只有background和border,那么GPU计算出圆角结果后并不需要与其他图层进行混合或者其他运算,GPU不需要开辟额外缓冲区保存结果,在它渲染完成后就可以将其清除。
    但是如果同时设置backgroud和contents,GPU绘制出背景图后,并不能马上将其提交渲染到屏幕上,额需要开辟离屏缓冲区保存背景图,等待GPU绘制完成contents时,在与background内容进行混合运算得到新的图层后,才能渲染到屏幕上,而这个时候,就产生了离屏渲染。

    3.解决方案
    1.让UI切圆角的图片,省时省力。
    2.使用UIBezierPath+CoreGraphics。

    imgView.image = [UIImage imageNamed:@"img"];
        UIGraphicsBeginImageContextWithOptions(imgView.bounds.size, NO, 1.0);
        [[UIBezierPath bezierPathWithRoundedRect:imgView.bounds cornerRadius:5] addClip];
        [imgView drawRect:imgView.bounds];
        imgView.image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    

    3.使用UIBezierPath+CAShapeLayer

        imgView.image = [UIImage imageNamed:@"img"];
        UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imgView.bounds cornerRadius:5];
        CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
        maskLayer.frame = imgView.bounds;
        maskLayer.path = maskPath.CGPath;
        imgView.layer.mask = maskLayer;
    

    4.采用三方库如YYImage

    YYImage内部实际使用了UIBezierPath+CoreGraphics生成了新的圆角图片。

    相关文章

      网友评论

          本文标题:离屏渲染--原理及解决方案

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