美文网首页
02总结--006--OpenGL 离屏渲染

02总结--006--OpenGL 离屏渲染

作者: 修_远 | 来源:发表于2020-07-07 22:36 被阅读0次

    [TOC]

    缓存!缓存!还是缓存!

    缓存
    1. 缓存是啥?cache、buffer。也许你没用过,但是你一定见过无数次这两个单词。
    2. 缓存有啥用?复用、效率提升,典型的空间换时间。

    这篇文章讲的内容就是从缓存开始的,下面来看看一些常见的缓存。

    class : cache_t

    cache_t 方法查找和转发流程

    在方法查找阶段:

    • 先从类的缓存中去取
    • 若没有找到,再从类的方法列表中找
    • 找到后,会将方法存到缓存中(方便下一次的读取,提高查找效率)

    http 缓存

    • cookies:用户信息,http中解决无法定位用户身份问题
    • body:数据缓存,避免每次都要从后台读取,使得数据的获取更快,效率更高,减少流量的浪费,降低服务器的负载

    SDWebImage :图片缓存

    SDWebImageSequenceDiagram
    • 内存缓存
    • 磁盘缓存

    NSStream : buffer

    这个例子和其他的稍微有点区别,应该叫缓冲区,其实也是一个缓存,但它的使用场景更加偏向于本章重点——离屏渲染

    NSStream:Input -> Output

    建立通道之后,数据流不是直接从 Input 输入到 Output,而是先输入到一个 data buffer 里面,然后 Output 从 data buffer 里面取

    NSStream:Input -> Buffer -> Output

    这样有什么好处呢?

    • 当接收端下行网络环境较差时,我们可以将更多的数据存到这个 buffer 里面,等到它网络恢复的时候再读取,不会造成数据的丢失
    • 同样,当发送端上行网络差时,接收端可以从 buffer 中取数据,降低网络对数据传输的影响

    UITableView : 缓存行高

    tableview的问题对于iOS开发者来说是老生常谈的问题了,其中有一条就是尽量避免使用 estimatedHeightForRowAtIndexPath 来设置高度,对于动态高度,我们一般会提前计算好高度,缓存起来,然后通过 heightForRowAtIndexPath 来设置高度。

    除了上面提到的一些常见的缓存,我们在实际开发中还有更多的自定义的缓存策略,比如组件化开发中,对组件的缓存。

    render buffer:渲染缓存(帧缓存)

    片元着色器给片元上色之后的像素怎么处理呢?直接显示到屏幕上吗?

    并不是,而是存在一个渲染缓存(帧缓存)里面,等到下一次runloop到来时,从帧缓存中读取数据,然后显示到屏幕上。

    image

    下图展示了苹果的双缓存技术,当只有一个缓存时,会出现掉帧等不良现象,所以苹果给了两个缓存区来存储数据。

    image

    offscreen buffer:离屏缓存

    上面说到了,苹果都给了两个缓存区来存储渲染数据,那为啥还会有离屏缓存呢?

    当需要绘制的图像由多个图层组成时,便需要将前面的渲染数据存起来,然后再对这些数据进行混合,得到新的数据,显示到屏幕上。离屏的操作便发生在存数据的时候。(帧缓存里面的渲染数据,使用完就会被丢弃,不会保存)

    • 案例1:mask 渲染流程
    image
    1. 渲染 layer mask 纹理,存储到离屏缓存区里面
    2. 渲染 layer content 纹理,存储到离屏缓存区里面
    3. 混合上面的纹理,存储到帧缓存区,等待显示
    • 案例2:UIBlurEffect 渲染流程
    image

    1-4:渲染内容、捕获内容、水平模糊、垂直模糊,这些结果都是存储在离屏缓存区里面;
    5:合成上面的结果,存储到帧缓存区,等待下一次显示。

    离屏渲染的理解

    关于离屏渲染以及渲染过程,可以查看02总结--005--OpenGL 渲染全解析[转载]这篇文章,这里主要是补充一些对离屏渲染的理解。比如前面讲了离屏渲染其实就是一个缓存区,这是一个很重要的理解思路。

    离屏渲染流程

    渲染流程
    • 正常流程:将内容渲染完成之后,不停地放入 Framebuffer 中,然后显示屏幕不断地从 Framebuffer 中读取内容,显示实时内容;
    • 离屏渲染:创建 Offscreenbuffer >> 将提前渲染好的内容放入其中 >> 等到合适的时机在将 OffScreenbuffer 中的内容进一步叠加、渲染 >> 最后将结果放入 Framebuffer 中;
    • 显示屏幕最后获取数据的来源都是 帧缓存Framebuffer;
    • Offscreenbuffer 只是一个临时存储渲染数据的地方

    离屏渲染原理

    • Layer的层级
      • backgroundColor
      • contents
      • borderWidth / borderColor
    layer的层级

    如上图所示,layer 由三层组成,我们设置圆角通常会首先像下面这行代码一样进行设置:

    view.layer.cornerRadius = 2
    
    cornerRadius官方解释
    • 设置 layer.cornerRadius 只会设置 backgroundColor 和 border 的圆角,不会设置 contents
    • 同时设置 layer.masksToBounds 才会设置 contents 的圆角。(对应view中的clipsToBounds属性)

    下面通过几个案例来理解这张图片

    1. 按钮存在背景图片
      1. 同时设置了 cornerRadiusclipsToBounds 这两个属性,会对 layer 的三层都起作用;
      2. button中设置了图片 setImage,说明它的contents中存在内容,需要渲染的内容有;(这里相当于给按钮添加了一个imageview)
      3. 对于复合图形的渲染,是需要借助 Offscreenbuffer 缓存的,所以触发了离屏渲染
    //1.按钮存在背景图片
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 30, 100, 100);
    btn1.layer.cornerRadius = 50;
    [self.view addSubview:btn1];
    
    [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
    btn1.clipsToBounds = YES;
    
    按钮中的imageview

    既然会造成离屏渲染,那如果直接对 imageview 设置圆角呢?结果是不会触发离屏渲染

    btn1.imageView.layer.cornerRadius = 50;
    btn1.imageView.layer.masksToBounds = YES;
    
    1. 按钮不存在背景图片
      1. 同时设置了 cornerRadiusclipsToBounds 这两个属性,会对 layer 的三层都起作用
      2. 而且还设置了 backgroundColor
      3. 但是 btn2 中并没有 contents 的元素,只有本身的 backgroundColor,是一个单一图层,所以不会触发离屏渲染
    //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;
    
    1. UIImageView 设置了图片+背景色;
      1. backgroundColor:[UIColor blueColor];
      2. contents: [UIImage imageNamed:@"btn.png"];
      3. 所以会离屏渲染
    //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"];
    
    1. UIImageView 只设置了图片,无背景色;
      1. 只有 contents: [UIImage imageNamed:@"btn.png"];
      2. 所以不会造成离屏渲染
    //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"];
    
    image

    【总结】

    通过上面的这几个例子来说,我们不能直接根据 圆角和裁剪 来判断是否触发离屏渲染,我们应该根据离屏渲染的原理来说明。

    离屏渲染优劣势

    • 优势:使用离屏渲染的原因

      1. 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
      2. 出于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
    • 劣势:避免离屏渲染的原因

      1. 离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)
      2. 并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。
      3. 可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。

    离屏渲染具体逻辑

    1. 画家算法

    刚才说了圆角加上 masksToBounds 的时候,因为 masksToBounds 会对 layer 上的所有内容进行裁剪,从而诱发了离屏渲染,那么这个过程具体是怎么回事呢,下面我们来仔细讲一下。

    图层的叠加绘制大概遵循“画家算法”,在这种算法下会按层绘制,首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分。

    image

    在普通的 layer 绘制中,上层的 sublayer 会覆盖下层的 sublayer,下层 sublayer 绘制完之后就可以抛弃了,从而节约空间提高效率。所有 sublayer 依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。假设我们需要绘制一个三层的 sublayer,不设置裁剪和圆角,那么整个绘制过程就如下图所示:

    [图片上传失败...(image-cde829-1594132539692)]

    而当我们设置了 cornerRadius 以及 masksToBounds 进行圆角 + 裁剪时,如前文所述,masksToBounds 裁剪属性会应用到所有的 sublayer 上。这也就意味着所有的 sublayer 必须要重新被应用一次圆角+裁剪,这也就意味着所有的 sublayer 在第一次被绘制完之后,并不能立刻被丢弃,而必须要被保存在 Offscreen buffer 中等待下一轮圆角+裁剪,这也就诱发了离屏渲染,具体过程如下:

    image

    实际上不只是圆角+裁剪,如果设置了透明度+组透明(layer.allowsGroupOpacity+layer.opacity),阴影属性(shadowOffset 等)都会产生类似的效果,因为组透明度、阴影都是和裁剪类似的,会作用与 layer 以及其所有 sublayer 上,这就导致必然会引起离屏渲染。

    离屏渲染案例

    mask rendering
    1. 渲染mask,存入离屏缓存区;
    2. 渲染layer,存入离屏缓存区;
    3. 读取离屏缓存区的数据,然后进行混合操作,将结果存入帧缓存区;
    4. 等待下一次 runloop到来,显示到屏幕上;
    image
    1. Content:渲染内容
    2. capture content:捕获内容
    3. Horizontal Blur:水平模糊
    4. Vertical Blur:垂直模糊
    5. Compositing Pass:合并过程
    6. 合并完成之后将结果存入帧缓存区,等待下一次 runloop到来,显示到屏幕上

    光栅化在离屏渲染中扮演的角色

    shouldRasterize 光栅化的使用建议:

    • 如果 layer 不能被复用,则没有必要打开光栅化;
    • 如果 layer 不是静态的,需要被频繁修改,比如处于动画之中,那么开启了离屏渲染反而会影响效率;
    • 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么它就会被丢弃调,无法进行复用;
    • 离屏渲染缓存内容有空间限制,超过 2.5 倍屏幕像素大小的话,也会失效,且无法进行复用;

    这里的 shouldRasterize 光栅化的使用建议 也是我们使用离屏渲染的建议。

    光栅化和离屏渲染的联系

    从上面离屏渲染的原理中可以知道,如果我们只是单一的图层显示,是不会触发离屏渲染的,而当我们开启光栅化之后,不管是单一图层还是复合图层,都会触发离屏渲染。
    所以光栅化的目的就是强制开启离屏渲染。

    • 最后一行打开了光栅化,所以也开启了离屏渲染
    image

    (离屏)缓存的时效性

    上面说的离屏渲染其实就是一个缓存,我们知道缓存一般时效性很低,对于Offscreenbuffer中存储的数据的缓存时间是 100ms

    常见圆角触发的情况以及处理办法

    常见圆角触发的情况

    1. 使用了 mask 的 layer(layer.mask)
    2. 需要进行裁剪的 layer(layer.masksToBounds / view.clipsToBounds)
    3. 设置了组透明度为 YES,并且透明度不为1 的layer(layer.allosGroupOpacity / layer.opacity)
    4. 添加了投影的 layer(layer.shadow*)
    5. 绘制了文字的 layer(UILabel,CATextLayer,Core Text等)

    圆角的处理办法

    1. 方案一【按钮上的图片】:使用 btn1.imageView

      上面的案例中也提到了,直接对 imageview 设置圆角,不要对button设置圆角

      btn1.imageView.layer.cornerRadius = 50;
      btn1.imageView.layer.masksToBounds = YES;
      
    2. 方案二:创建一个圆角图片

      @implementation UIImage (CornerRadius)
      
      - (UIImage *)roundedCornerImageWithCornerRadius:(CGFloat)cornerRadius {
          CGFloat w = self.size.width;
          CGFloat h = self.size.height;
          CGFloat scale = UIScreen.mainScreen.scale;
          // 防止圆角半径小于0, 或者大于宽/高中较小值的一半
          cornerRadius = MAX(cornerRadius, 0);
          cornerRadius = MIN(cornerRadius, MIN(w, h)/2);
      
          UIImage* image = nil;
          CGRect imageFrame = CGRectMake(0, 0, w, h);
          UIGraphicsBeginImageContextWithOptions(self.size, NO, scale);
          UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius];
          [path addClip];
          [self drawInRect:imageFrame];
          image = UIGraphicsGetImageFromCurrentImageContext();
          UIGraphicsEndImageContext();
          return image;
      }
      
      @end
      
    3. 方案三:设置Imageview的image

      @implementation UIImageView (MaskBounds)
      
      - (void)addMaskToBounds:(CGRect)maskBounds WithCornerRadius:(CGFloat)cornerRadius {
          CGFloat w = maskBounds.size.width;
          CGFloat h = maskBounds.size.height;
          CGFloat scale = UIScreen.mainScreen.scale;
          CGSize size = maskBounds.size;
          CGRect imageRect = CGRectMake(0, 0, w, h);
          // 防止圆角半径小于0, 或者大于宽/高中较小值的一半
          cornerRadius = MAX(cornerRadius, 0);
          cornerRadius = MIN(cornerRadius, MIN(w, h)/2);
      
          UIImage* image = self.image;
          UIGraphicsBeginImageContextWithOptions(size, NO, scale);
          UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:cornerRadius];
          [path addClip];
          [image drawInRect:imageRect];
          self.image = UIGraphicsGetImageFromCurrentImageContext();
          UIGraphicsEndImageContext();
      }
      
      @end
      

    自测也是总结

    以下内容都是笔者的理解,欢迎留言给出其他解释。

    1. CPU 和 GPU 的设计目的分别是什么?

      • CPU 处理逻辑、控制核心、依赖高
      • GPU 处理大量简单的计算、依赖低
    2. CPU 和 GPU 哪个的 Cache\ALU\Control unit 的比例更高?

      • CPU 中缓存和控制单元比例高
      • GPU 中计算单元比例高
    3. 计算机图像渲染流水线的大致流程是什么?

      • Application:处理事件、提交动画(界面可能会发生变化);
      • CoreAnimation:CPU处理显示内容的前置计算,例如布局计算、解码等任务,然后将图层打包传递到下一层(渲染层);
      • Render server:GPU渲染流程。(过程:顶点着色器->光栅化->片元着色器->存到帧缓存区,结果:原始图元->新图元->片元->像素->位图
      • 等待下一个runloop的到来,将位图显示到屏幕上
    4. Framebuffer 帧缓冲器的作用是什么?

      • 存储GPU渲染结果(位图),等待下一个runloop的到来,显示到屏幕上
    5. Screen Tearing 屏幕撕裂是怎么造成的?

      • 电子束在扫描新的一帧时,位图还没有处理好
      • 扫描到中间的时候,位图处理好了
      • 这时,上半部是上一帧的画面,下半部是这一帧的画面,所以造成撕裂
    6. 如何解决屏幕撕裂的问题?

      • 垂直同步(Vsync)+双缓存区(Double Buffering)
    7. 掉帧是怎么产生的?

      • 由于同步的问题,帧缓存区中画面的显示是按顺序显示的
      • 当CPU+GPU在16.67ms内没有完成一帧的计算时
      • 下一次runloop的到来,并不能从帧缓存区中拿到新图像
      • 所以显示的还是上一次的画面,所以造成掉帧
    8. CoreAnimation 的职责是什么?

      • 主要职责包含:渲染、构建和实现动画。
      • 是 app 界面渲染和构建的最基础架构
      • 尽可能快地组合屏幕上不同的可视内容,并且被存储为树状层级结构
      • 这个树也形成了 UIKit 以及在 iOS 应用程序当中你所能在屏幕上看见的一切的基础
    9. UIView 和 CALayer 是什么关系?有什么区别?

      • 相同的层级结构:我们对 UIView 的层级结构非常熟悉,由于每个 UIView 都对应 CALayer 负责页面的绘制,所以 CALayer 也具有相应的层级结构。
      • 部分效果的设置:因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。
      • 是否响应点击事件:CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
      • 不同继承关系:CALayer 继承自 NSObject,UIView 由于要负责交互事件,所以继承自 UIResponder。
    10. 为什么会同时有 UIView 和 CALayer,能否合成一个?

      • 单一职责原则,UIView 和 CALayer 分别负责自己独立的职责
      • CALayer的复用,CALayer除了服务于UIKit之外,还服务于AppKit,在mac开发中也会用到
    11. 渲染流水线中,CPU 会负责哪些任务?

      • 点击事件的处理
      • 显示内容的前置计算,例如布局计算、图片解码等任务
    12. 离屏渲染为什么会有效率问题?

      1. 离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)
      2. 并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。
      3. 可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。
    13. 什么时候应该使用离屏渲染?

      1. 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
      2. 出于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
    14. shouldRasterize 光栅化是什么?

      • 光栅化的目的就是强制开启离屏渲染。
      • 如果 layer 不能被复用,则没有必要打开光栅化;
      • 如果 layer 不是静态的,需要被频繁修改,比如处于动画之中,那么开启了离屏渲染反而会影响效率;
      • 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么它就会被丢弃调,无法进行复用;
      • 离屏渲染缓存内容有空间限制,超过 2.5 倍屏幕像素大小的话,也会失效,且无法进行复用;
    15. 有哪些常见的触发离屏渲染的情况?

      1. 使用了 mask 的 layer(layer.mask)
      2. 需要进行裁剪的 layer(layer.masksToBounds / view.clipsToBounds)
      3. 设置了组透明度为 YES,并且透明度不为1 的layer(layer.allosGroupOpacity / layer.opacity)
      4. 添加了投影的 layer(layer.shadow*)
      5. 绘制了文字的 layer(UILabel,CATextLayer,Core Text等)
    16. cornerRadius 设置圆角会触发离屏渲染吗?

      • 不会触发。
      • 设置了 maskToBounds(clipsToBounds)才有可能触发
    17. 圆角触发的离屏渲染有哪些解决方案?

      • 如果是图片,可以让UI提供带圆角的图片
      • 先将图片圆角切好,然后使用切好圆角的图片
      • 如果是按钮背景图,可以直接设置imageview的圆角
      • 绘制子layer,插入一个子layer作为显示层(类比于按钮上的imageview)
    18. 重写 drawRect 方法会触发离屏渲染吗?

      • 不会触发离屏渲染
      • drawRect 是在CPU中执行的

    相关文章

      网友评论

          本文标题:02总结--006--OpenGL 离屏渲染

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