02总结--006--OpenGL 离屏渲染
[TOC]
缓存!缓存!还是缓存!
缓存- 缓存是啥?cache、buffer。也许你没用过,但是你一定见过无数次这两个单词。
- 缓存有啥用?复用、效率提升,典型的空间换时间。
这篇文章讲的内容就是从缓存开始的,下面来看看一些常见的缓存。
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下图展示了苹果的双缓存技术,当只有一个缓存时,会出现掉帧等不良现象,所以苹果给了两个缓存区来存储数据。
imageoffscreen buffer:离屏缓存
上面说到了,苹果都给了两个缓存区来存储渲染数据,那为啥还会有离屏缓存呢?
当需要绘制的图像由多个图层组成时,便需要将前面的渲染数据存起来,然后再对这些数据进行混合,得到新的数据,显示到屏幕上。离屏的操作便发生在存数据的时候。(帧缓存里面的渲染数据,使用完就会被丢弃,不会保存)
- 案例1:mask 渲染流程
- 渲染 layer mask 纹理,存储到离屏缓存区里面
- 渲染 layer content 纹理,存储到离屏缓存区里面
- 混合上面的纹理,存储到帧缓存区,等待显示
- 案例2:UIBlurEffect 渲染流程
1-4:渲染内容、捕获内容、水平模糊、垂直模糊,这些结果都是存储在离屏缓存区里面;
5:合成上面的结果,存储到帧缓存区,等待下一次显示。
离屏渲染的理解
关于离屏渲染以及渲染过程,可以查看02总结--005--OpenGL 渲染全解析[转载]这篇文章,这里主要是补充一些对离屏渲染的理解。比如前面讲了离屏渲染其实就是一个缓存区,这是一个很重要的理解思路。
离屏渲染流程
渲染流程- 正常流程:将内容渲染完成之后,不停地放入 Framebuffer 中,然后显示屏幕不断地从 Framebuffer 中读取内容,显示实时内容;
- 离屏渲染:创建 Offscreenbuffer >> 将提前渲染好的内容放入其中 >> 等到合适的时机在将 OffScreenbuffer 中的内容进一步叠加、渲染 >> 最后将结果放入 Framebuffer 中;
- 显示屏幕最后获取数据的来源都是 帧缓存Framebuffer;
- Offscreenbuffer 只是一个临时存储渲染数据的地方
离屏渲染原理
- Layer的层级
- backgroundColor
- contents
- borderWidth / borderColor
如上图所示,layer 由三层组成,我们设置圆角通常会首先像下面这行代码一样进行设置:
view.layer.cornerRadius = 2
cornerRadius官方解释
- 设置
layer.cornerRadius
只会设置 backgroundColor 和 border 的圆角,不会设置 contents - 同时设置
layer.masksToBounds
才会设置 contents 的圆角。(对应view中的clipsToBounds属性)
下面通过几个案例来理解这张图片
- 按钮存在背景图片
- 同时设置了
cornerRadius
和clipsToBounds
这两个属性,会对 layer 的三层都起作用; - button中设置了图片
setImage
,说明它的contents中存在内容,需要渲染的内容有;(这里相当于给按钮添加了一个imageview) - 对于复合图形的渲染,是需要借助
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;
- 按钮不存在背景图片
- 同时设置了
cornerRadius
和clipsToBounds
这两个属性,会对 layer 的三层都起作用 - 而且还设置了
backgroundColor
, - 但是 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;
- UIImageView 设置了图片+背景色;
- backgroundColor:[UIColor blueColor];
- contents: [UIImage imageNamed:@"btn.png"];
- 所以会离屏渲染
//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"];
- UIImageView 只设置了图片,无背景色;
- 只有 contents: [UIImage imageNamed:@"btn.png"];
- 所以不会造成离屏渲染
//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
【总结】
通过上面的这几个例子来说,我们不能直接根据 圆角和裁剪 来判断是否触发离屏渲染,我们应该根据离屏渲染的原理来说明。
离屏渲染优劣势
-
优势:使用离屏渲染的原因
- 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
- 出于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
-
劣势:避免离屏渲染的原因
- 离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)
- 并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。
- 可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。
离屏渲染具体逻辑
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- 渲染mask,存入离屏缓存区;
- 渲染layer,存入离屏缓存区;
- 读取离屏缓存区的数据,然后进行混合操作,将结果存入帧缓存区;
- 等待下一次 runloop到来,显示到屏幕上;
- Content:渲染内容
- capture content:捕获内容
- Horizontal Blur:水平模糊
- Vertical Blur:垂直模糊
- Compositing Pass:合并过程
- 合并完成之后将结果存入帧缓存区,等待下一次 runloop到来,显示到屏幕上
光栅化在离屏渲染中扮演的角色
shouldRasterize 光栅化的使用建议:
- 如果 layer 不能被复用,则没有必要打开光栅化;
- 如果 layer 不是静态的,需要被频繁修改,比如处于动画之中,那么开启了离屏渲染反而会影响效率;
- 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么它就会被丢弃调,无法进行复用;
- 离屏渲染缓存内容有空间限制,超过 2.5 倍屏幕像素大小的话,也会失效,且无法进行复用;
这里的 shouldRasterize 光栅化的使用建议
也是我们使用离屏渲染的建议。
光栅化和离屏渲染的联系
从上面离屏渲染的原理中可以知道,如果我们只是单一的图层显示,是不会触发离屏渲染的,而当我们开启光栅化之后,不管是单一图层还是复合图层,都会触发离屏渲染。
所以光栅化的目的就是强制开启离屏渲染。
- 最后一行打开了光栅化,所以也开启了离屏渲染
(离屏)缓存的时效性
上面说的离屏渲染其实就是一个缓存,我们知道缓存一般时效性很低,对于Offscreenbuffer中存储的数据的缓存时间是 100ms
常见圆角触发的情况以及处理办法
常见圆角触发的情况
- 使用了 mask 的 layer(layer.mask)
- 需要进行裁剪的 layer(layer.masksToBounds / view.clipsToBounds)
- 设置了组透明度为 YES,并且透明度不为1 的layer(layer.allosGroupOpacity / layer.opacity)
- 添加了投影的 layer(layer.shadow*)
- 绘制了文字的 layer(UILabel,CATextLayer,Core Text等)
圆角的处理办法
-
方案一【按钮上的图片】:使用 btn1.imageView
上面的案例中也提到了,直接对 imageview 设置圆角,不要对button设置圆角
btn1.imageView.layer.cornerRadius = 50; btn1.imageView.layer.masksToBounds = YES;
-
方案二:创建一个圆角图片
@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
-
方案三:设置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
自测也是总结
以下内容都是笔者的理解,欢迎留言给出其他解释。
-
CPU 和 GPU 的设计目的分别是什么?
- CPU 处理逻辑、控制核心、依赖高
- GPU 处理大量简单的计算、依赖低
-
CPU 和 GPU 哪个的 Cache\ALU\Control unit 的比例更高?
- CPU 中缓存和控制单元比例高
- GPU 中计算单元比例高
-
计算机图像渲染流水线的大致流程是什么?
- Application:处理事件、提交动画(界面可能会发生变化);
- CoreAnimation:CPU处理显示内容的前置计算,例如布局计算、解码等任务,然后将图层打包传递到下一层(渲染层);
- Render server:GPU渲染流程。(
过程:顶点着色器->光栅化->片元着色器->存到帧缓存区,结果:原始图元->新图元->片元->像素->位图
) - 等待下一个runloop的到来,将位图显示到屏幕上
-
Framebuffer 帧缓冲器的作用是什么?
- 存储GPU渲染结果(位图),等待下一个runloop的到来,显示到屏幕上
-
Screen Tearing 屏幕撕裂是怎么造成的?
- 电子束在扫描新的一帧时,位图还没有处理好
- 扫描到中间的时候,位图处理好了
- 这时,上半部是上一帧的画面,下半部是这一帧的画面,所以造成撕裂
-
如何解决屏幕撕裂的问题?
- 垂直同步(Vsync)+双缓存区(Double Buffering)
-
掉帧是怎么产生的?
- 由于同步的问题,帧缓存区中画面的显示是按顺序显示的
- 当CPU+GPU在16.67ms内没有完成一帧的计算时
- 下一次runloop的到来,并不能从帧缓存区中拿到新图像
- 所以显示的还是上一次的画面,所以造成掉帧
-
CoreAnimation 的职责是什么?
- 主要职责包含:渲染、构建和实现动画。
- 是 app 界面渲染和构建的最基础架构
- 尽可能快地组合屏幕上不同的可视内容,并且被存储为树状层级结构
- 这个树也形成了 UIKit 以及在 iOS 应用程序当中你所能在屏幕上看见的一切的基础
-
UIView 和 CALayer 是什么关系?有什么区别?
- 相同的层级结构:我们对 UIView 的层级结构非常熟悉,由于每个 UIView 都对应 CALayer 负责页面的绘制,所以 CALayer 也具有相应的层级结构。
- 部分效果的设置:因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。
- 是否响应点击事件:CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
- 不同继承关系:CALayer 继承自 NSObject,UIView 由于要负责交互事件,所以继承自 UIResponder。
-
为什么会同时有 UIView 和 CALayer,能否合成一个?
- 单一职责原则,UIView 和 CALayer 分别负责自己独立的职责
- CALayer的复用,CALayer除了服务于UIKit之外,还服务于AppKit,在mac开发中也会用到
-
渲染流水线中,CPU 会负责哪些任务?
- 点击事件的处理
- 显示内容的前置计算,例如布局计算、图片解码等任务
-
离屏渲染为什么会有效率问题?
- 离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)
- 并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。
- 可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。
-
什么时候应该使用离屏渲染?
- 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
- 出于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
-
shouldRasterize 光栅化是什么?
- 光栅化的目的就是强制开启离屏渲染。
- 如果 layer 不能被复用,则没有必要打开光栅化;
- 如果 layer 不是静态的,需要被频繁修改,比如处于动画之中,那么开启了离屏渲染反而会影响效率;
- 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么它就会被丢弃调,无法进行复用;
- 离屏渲染缓存内容有空间限制,超过 2.5 倍屏幕像素大小的话,也会失效,且无法进行复用;
-
有哪些常见的触发离屏渲染的情况?
- 使用了 mask 的 layer(layer.mask)
- 需要进行裁剪的 layer(layer.masksToBounds / view.clipsToBounds)
- 设置了组透明度为 YES,并且透明度不为1 的layer(layer.allosGroupOpacity / layer.opacity)
- 添加了投影的 layer(layer.shadow*)
- 绘制了文字的 layer(UILabel,CATextLayer,Core Text等)
-
cornerRadius 设置圆角会触发离屏渲染吗?
- 不会触发。
- 设置了 maskToBounds(clipsToBounds)才有可能触发
-
圆角触发的离屏渲染有哪些解决方案?
- 如果是图片,可以让UI提供带圆角的图片
- 先将图片圆角切好,然后使用切好圆角的图片
- 如果是按钮背景图,可以直接设置imageview的圆角
- 绘制子layer,插入一个子layer作为显示层(类比于按钮上的imageview)
-
重写 drawRect 方法会触发离屏渲染吗?
- 不会触发离屏渲染
- drawRect 是在CPU中执行的