美文网首页面试
ios--离屏渲染详解

ios--离屏渲染详解

作者: Harry__Li | 来源:发表于2021-05-17 20:18 被阅读0次

    目录:

    • 1.图像显示原理
    • 2.图像显示原理
      • 2.1 图像到屏幕的流程
      • 2.2 显示器显示的流程
    • 3.卡顿、掉帧
      • 3.1 垂直同步 Vsync + 双缓冲机制 Double Buffering
      • 2.3 掉帧和屏幕卡顿的本质
    • 4.离屏渲染
      • 4.1 什么事离屏渲染、离屏渲染的过程
      • 4.2 既然离屏渲染影响界面,为什么还要用
    • 5.触发离屏渲染
    • 6.如何优化
    1.引言

    先来聊聊为什么要了解离屏渲染?
    看看现在app开发的大环境,14年的时候在深圳,基本上每个公司都要做一个app。不做一个app你都不一定能拉倒更多的投资。再看看现在,死了一大半,现在的用户也不想去下载太多的app。一般手机上只留一些常用的,基本全是大厂的app。然后ios这行问的也就越来越难。性能优化这个绝对会问,在网上也有许多性能优化的总结,但是你不能不知道为什么这么做能优化,要知道其为什么。那么,这时候你就需要知道界面是怎么渲染的,什么时候会掉帧,什么时候会卡顿,这些都使得我们非常有必要去了解离屏渲染。
    离屏渲染过程

    2.图像显示原理
    2.1 图像到屏幕的流程

    先来看一张图,我们结合这张图来说


    Core Animation 流水线.png

    首先要明白的一个东西是Render Server 进程,app本身其实并不负责渲染,渲染是有独立的进程负责的,它就是Render Server 。

    当我们在代码里设置修改了UI界面的时候,其实它本质是通过Core Animation修改CALayer。在后续的核心动画总结中 我们会说到UIView和CALayer的关系,以及核心动画的设置等等,这个知识点有点多,需要单独详细的总结出来。所以最后按照图片中的流程显示。

    • 首先,有app处理事件(Handle Events),例如:用户点击了一个按钮,它会触发其他的视图的一个动画等
    • 其次,app通过CPU完成对显示内容的计算 例如:视图的创建,视图的布局 图片文本的绘制等。在完成了对显示内容的计算之后,app对图层进行打包,并在下一次Runloop时,将其发送至Render Server
    • 上面我们提到,Render Server负责渲染。Render Server通过执行Open GL、Core Graphics Metal相关程序。 调用GPU
    • GPU在物理层完成了对图像的渲染。

    说到这我们就要停一下,我们来看下一个图


    GPU.png

    上面的流程图 细化了GPU到控制器的这一个过程。
    GPU 拿到位图后执行顶点着色、图元装配、光栅化、片段着色等,最后将渲染的结果交到了Frame Buffer(帧缓存区当中)
    然后视频控制器从帧缓存区中拿到要显示的对象,显示到屏幕上
    图片中的黄色虚线暂时不用管,下面在说垂直同步信号的时候,就明白了。
    这是从我们代码中设置UI,然后到屏幕的一个过程。

    2.2 显示器显示的过程

    现在从帧缓存中拿到了渲染的视图,又该怎么显示到显示器上面呢?

    先来看一张图 扫描显示.png
    从图中我们也能大致的明白显示的一个过程。

    显示器的电子束从屏幕的左上方开始逐行显示,当第一行扫描完之后接着第二行 又是从左到右,就这样一直到屏幕的最下面扫描完成。我们都知道。手机它是有屏幕的刷新次数的。安卓的现在好多是120的,ios是60。1秒刷新60次,当我们扫描完成以后,屏幕刷新,然后视图就会显示出来。

    3.UI卡顿 掉帧
    3.1垂直同步 Vsync + 双缓冲机制 Double Buffering

    首先我们了解了上面渲染的过程以后,需要考虑遇到一些特别的情况下,该怎么办?在我们代码里写了一个很复杂的UI视图,然后CPU计算布局、GPU渲染,最后放到缓存区。如果在电子束开始扫描新的一帧时,位图还没有渲染好,而是在扫描到屏幕中间时才渲染完成,被放入帧缓冲器中 。
    那么已扫描的部分就是上一帧的画面,而未扫描的部分就是新一帧的图像,这样是不是就造成了屏幕撕裂了。

    但是,在我们平常开发的过程遇到过屏幕撕裂的问题吗?没有吧,这是为什么呢?
    显然是苹果做了优化操作了。也就是垂直同步 Vsync + 双缓冲机制 Double Buffering。

    垂直同步 Vsync
    垂直同步 Vsync相当于给帧缓存加了锁,还记得上面说到的那个黄色虚线嘛,在我们扫描完一帧以后,就会发出一个垂直同步的信号,通知开始扫描下一帧的图像了。他就像一个位置秩序的,你得给我排队一个一个来,别插队。插队的后果就是屏幕撕裂。
    双缓冲机制 Double Buffering
    扫描显示排队进行了,这样在进行下一帧的位图传入的时候,也就意味着我要立刻拿到位图。不能等CPU+GPU计算渲染后再给位图,这样就影响性能。要怎么解决这个问题呢?肯定是 在你快要渲染之前你就要把这些都完成了。你就像排队打针一样,为了节省时间肯定事先都会挽起袖子,到医生那时,直接一针下去了事。扯远了 哈哈。想预先渲染好,就需要另外一个缓存来放下一帧的位图,在它需要扫描的时候,再把渲染好的位图给了帧缓存,帧缓存拿到以后 开始快乐的扫描 显示。
    一个图解释

    双缓存.png
    3.2 掉帧卡顿

    垂直同步和双缓存机制完美的解决了屏幕撕裂的问题,但是又引出一个新的问题:掉帧。
    掉帧是什么意思呢?从网上copy了一份图


    掉帧.png

    其实很好理解,上面我们说了ios的屏幕刷新是60次,那么在一次刷新的过程中,我们CPU+GPU它没有把新渲染的位图放到帧缓存区,这时候是不是还是显示的原来的图像。当下刷新下一帧的时候,拿到了新的位图,这里是不是就丢失了一帧。

    卡顿的根本原因:
    CPU和GPU渲染流水线耗时过长 掉帧
    我们平常写界面的时候,通过一些开源的库或者自己使用runloop写的库来检测界面卡顿的时候,屏幕刷新率在50以上就很可以了。一般人哪能体验到掉了10帧。你要刷新率是30,那卡顿想过就很明显了。

    4 离屏渲染
    4.1什么是离屏渲染 离屏渲染的过程

    是指在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作.
    过程:首先会创建一个当前屏幕缓冲区以外的新缓存区,屏幕渲染会有一个上下文环境,离屏渲染的过程就是切花上下文环境,现充当前屏幕切换到离屏,等结束以后又将上下文切换回来。所以需要更长的时间来处理。时间一长就可能造成掉帧。
    并且 Offscreen Buffer离屏缓存 本身就需要额外的空间,大量的离屏渲染可能造成内存过大的压力。而且离屏缓存区并不是没有限制大小的,它是不能超过屏幕总像素的2.5倍。

    4.2为什么要使用离屏渲染

    1.一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
    2.处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
    当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前(下一个VSync信号开始前)不能直接在屏幕中绘制,所以就需要屏幕外渲染。

    5.触发离屏渲染
    1. 为图层设置遮罩(layer.mask)
    2. 图层的layer. masksToBounds/view.clipsToBounds属性设置为true
    3. 将图层layer. allowsGroupOpacity设置为yes和layer. opacity<1.0
    4. 为图层设置阴影(layer.shadow)
    5. 为图层设置shouldRasterize光栅化
      6 复杂形状设置圆角等
      7 渐变
      8 文本(任何种类,包括UILabel,CATextLayer,Core Text等)
      9 使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。
    6 离屏渲染的优化
    1 圆角优化

    方法一

    iv.layer.cornerRadius = 30;
    iv.layer.masksToBounds = YES;
    

    方法二
    利用mask设置圆角,利用贝塞斯曲线和CAShapeLayer来完成

    CAShapeLayer *mask1 = [[CAShapeLayer alloc] init];
    mask1.opacity = 0.5;
    mask1.path = [UIBezierPath bezierPathWithOvalInRect:iv.bounds].CGPath;
    iv.layer.mask = mask1;
    

    方法三
    利用CoreGraphics画一个圆形上下文,然后把图片绘制上去

    - (void)setCircleImage
    {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            UIImage * circleImage = [image imageWithCircle];
            dispatch_async(dispatch_get_main_queue(), ^{
                imageView.image = circleImage;
            });
        });
    }
    
    
    #import "UIImage+Addtions.h"
    @implementation UIImage (Addtions)
    //返回一张圆形图片
    - (instancetype)imageWithCircle
    {
        UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
        [path addClip];
        [self drawAtPoint:CGPointZero];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
    }
    
    shadows(阴影)

    设置阴影后,设置CALayer的shadowPath

    view.layer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath;
    
    mask(遮罩)

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

    sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
    [view.layer addSublayer:sublayer];
    
    allowsGroupOpacity(组不透明)

    关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度

    edge antialiasing(抗锯齿)

    不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

    当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便

    view.layer.shouldRasterize = true;
    view.layer.rasterizationScale = view.layer.contentsScale;
    

    如果视图内容是动态变化的,例如cell中的图片,这个时候使用光栅化会增加系统负荷。

    相关文章

      网友评论

        本文标题:ios--离屏渲染详解

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