离屏渲染

作者: foreverSun_122 | 来源:发表于2018-04-23 19:04 被阅读0次

    图像显示原理

    enter image description here

    图像显示的大概流程:

    1. 程序运行从内存中读取数据
      • 对图片进行解压得到像素数据,若GPU不支持图片的颜色格式,CPU需要进行格式转换
      • CoreText和CoreGraphics跟进文本内容生成位图
    2. 然后解压后的数据或位图通过GPU Bus上传到GPU,GPU需要将每一个frame的纹理(位图)合成在一起(一秒60次)。每一个纹理会占用VRAM(video RAM),所以需要给 GPU 同时保持纹理的数量做一个限制。GPU 在合成方面非常高效,但是某些合成任务却比其他更复杂,并且 GPU在 16.7ms(1/60s)内能做的工作也是有限的。
    CPU的工作
    • 对象的创建、调整和销毁
    • 布局计算
    • 文本计算
    • 文本渲染
    • 图片的解码
    • 图像的绘制
    GPU的工作
    • 纹理的渲染
    • 视图合成
    • 图形生成

    GPU显示图像

    enter image description here
    1. CPU 计算好显示内容提交到 GPU
    2. GPU 渲染完成后将渲染结果放入帧缓冲区
    3. 随后视频控制器会按照VSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

        在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。

    垂直同步机制
    enter image description here

        当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。
        为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync)
        首先从过去的CRT显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。
        当开启垂直同步后,GPU 会等待显示器的VSync信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

    卡顿的产生

    enter image description here

        在VSync信号到来后,系统图形服务会通过CADisplayLink等机制通知App,App主线程开始在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
        从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。

    离屏渲染

    GPU的渲染方式

    • On-Screen Rendering(当前屏幕渲染):指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。


      enter image description here
    • Off-Screen Rendering (离屏渲染),指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。


      enter image description here

          OffScreen Rendering 则多了一个步骤,GPU 会先创建一个屏外缓冲区(OffScreenBuffer),然后在其中进行渲染,最后将渲染结果提交到帧缓冲区内(FrameBuffer);这其中还涉及到了两次上下文的转换,首先把当前上下文转换到屏外缓冲区(OffScreenBuffer),然后又转换到帧缓冲区(FrameBuffer)。整个过程会造成很大的消耗。例如蒙板操作:
          在前两个渲染通道中,GPU分别得到了纹理(texture,也就是那个相机图标)和layer(蓝色的蒙版)的渲染结果。但这两个渲染结果没有直接放入Render Buffer中,也就表示这是离屏渲染。直到第三个渲染通道,才把两者组合起来放入Render Buffer中。离屏渲染意味着把渲染结果临时保存,等用到时再取出,因此相对于普通渲染更占用资源。

    CPU 渲染
        如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。由CPU处理的一种特殊渲染方式,在App内同步完成,渲染得到的bitmap最后再交由GPU用于显示,由于CPU自身做渲染的性能也不好,所以这种方式也是需要尽量避免的。

    enter image description here

    为何需要离屏渲染

        一些复杂的效果,如:圆角,阴影,遮罩,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,无法直接渲染出结果,所以就需要屏幕外渲染被唤起。

        屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。

        所以当使用离屏渲染的时候会很容易造成性能消耗,因为在OPENGL里离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。

    触发离屏渲染的操作
    • shouldRasterize(光栅化)
    • masks(遮罩)
    • shadows(阴影)
    • edge antialiasing(抗锯齿)
    • group opacity(不透明)
    • 复杂形状设置圆角等
    光栅化:

    概念:将图转化为一个个栅格组成的图象。
    特点:每个元素对应帧缓冲区中的一像素。
        shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。相当于光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用。
        “Color Hits Green and Misses Red”可以检查当前场景下光栅化操作,绿色表示缓存被复用,红色表示缓存在被重复创建。
        如果光栅化的层变红得太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁,红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。

    注意:
    对于经常变动的内容,不要开启光栅化,则会造成大量的离屏渲染,降低图形性能。

    圆角的设置

    1. 使用cornerRadius

      self.layer.cornerRadius = cornerRadius;    self.layer.masksToBounds = YES;    //防止子view边界超过父view
      //self.clipsToBounds = YES;
      self.layer.shouldRasterize = YES;       //光栅化
      
      • UIView的clipsToBounds与CALayer的maskToBounds的作用一致,防止子view边界超过父view
      • cornerRadius默认情况下只对背景色和border起作用
      • 如果最后设置了 shouldRasterize 为 YES,那也要记住设置 rasterizationScale 为 contentsScale
    1. 使用贝塞尔曲线 + maskLayer

      - (void)setRoundRect:(CGRect)frame cornerRadius:(CGFloat)cornerRadius {
          CGRect maskFrame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));
          UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:maskFrame  cornerRadius:cornerRadius];
          CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
          maskLayer.frame = maskFrame;
          maskLayer.path = path.CGPath;
          self.layer.mask = maskLayer;
      }
      

      UIBezierPath对CoreGraphics进行了一层封装

    2. 使用CoreGraphics绘制圆角图片做背景(CPU渲染)

      - (void)drawRoundCornerWithCornerRadius:(CGFloat)cornerRadius {
      
          CGFloat width = CGRectGetWidth(self.frame);
          CGFloat height = CGRectGetHeight(self.frame);
      
          UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
          [self addSubview:imageView];
          dispatch_async(dispatch_queue_create("backgroundQueue", DISPATCH_QUEUE_CONCURRENT), ^{
          UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, [UIScreen mainScreen].scale);
              CGContextRef context = UIGraphicsGetCurrentContext();
      
              CGContextMoveToPoint(context, 0, 0);
          
              CGContextAddArcToPoint(context, width, 0, width, height, cornerRadius);
              CGContextAddArcToPoint(context, width, height, 0, height, cornerRadius);
              CGContextAddArcToPoint(context, 0, height, 0, 0, cornerRadius);
              CGContextAddArcToPoint(context, 0, 0, width, 0, cornerRadius);
          
              CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
              CGContextDrawPath(context, kCGPathStroke);
          
              UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
              UIGraphicsEndImageContext();
              dispatch_async(dispatch_get_main_queue(), ^{
              imageView.image = image;
              });
          });
      }
      

      CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程

    3. 将图片剪切为圆角(针对图片)

      - (UIImage *)setRoundCornerRadius:(CGFloat)cornerRadius {
          UIImage *image = nil;
          CGRect imageFrame = CGRectMake(0, 0, self.size.width, self.size.height);
          UIGraphicsBeginImageContextWithOptions(self.size, NO, [UIScreen mainScreen].scale);
          UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius];
          [path addClip];
          [self drawInRect:imageFrame];
          image = UIGraphicsGetImageFromCurrentImageContext();
          UIGraphicsEndImageContext();
          return image;
      }
      
    4. 使用圆角图片作为蒙板

    如何避免离屏渲染

    1. 圆角视图较少,使用使用cornerRadius
    2. UIImageView 的圆角通过直接截取图片实现,其它视图的圆角可以通过 Core Graphics 画出圆角矩形实现。
    3. 对于图形采用异步绘制
    4. 直接使用圆角素材作为背景

    参考资料

    iOS离屏渲染优化
    绘制像素到屏幕上
    关于性能的一些问题(iOS)
    解决常见的masksToBounds离屏渲染带来的性能损耗
    iOS 离屏渲染的研究
    小心别让圆角成了你列表的帧数杀手
    iOS 高效添加圆角效果实战讲解
    UIKit性能调优实战讲解
    iOS 保持界面流畅的技巧
    iOS开发:关于图形渲染以及界面优化的的一些想法
    iOS图形渲染分析

    相关文章

      网友评论

        本文标题:离屏渲染

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