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

2020-07-07  本文已影响0人  修_远

[TOC]

缓存!缓存!还是缓存!

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

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

class : cache_t

cache_t 方法查找和转发流程

在方法查找阶段:

http 缓存

SDWebImage :图片缓存

SDWebImageSequenceDiagram

NSStream : buffer

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

NSStream:Input -> Output

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

NSStream:Input -> Buffer -> Output

这样有什么好处呢?

UITableView : 缓存行高

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

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

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

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

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

image

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

image

offscreen buffer:离屏缓存

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

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

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

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

离屏渲染的理解

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

离屏渲染流程

渲染流程

离屏渲染原理

layer的层级

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

view.layer.cornerRadius = 2
cornerRadius官方解释

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

  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. 画家算法

刚才说了圆角加上 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 光栅化的使用建议:

这里的 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中执行的
上一篇下一篇

猜你喜欢

热点阅读