视觉 - (2)离屏渲染
一、什么是离屏渲染
1、 离屏渲染(offscreen-rendering)顾名思义为屏幕外的渲染,即渲染的结果不会直接呈现到当前屏幕上,而是等待合适的时机才会被显示。

如上图所示:正常情况下,当前屏幕显示的内容,由GPU渲染完成后放到当前屏幕的帧缓存区,不需要额外的渲染空间。我们知道iphone和主流的android设备的屏幕刷新率是60Hz,也就是刷新一帧的时间是16.67ms,每隔这段时间视频控制器就会去读一次缓存区的内容来显示。
假如GPU遇到性能瓶颈,导致无法在一帧内更新渲染结果到帧缓存区,那么从缓存区读到的会是上一帧的内容,导致帧率降低界面卡顿,也就是掉帧。
苹果或者安卓对于页面流畅度的要求是非常苛刻的,如果页面布局比较复杂,硬件遇到瓶颈,不能任由它卡顿。离屏渲染的机制就被引入,它会触发比较消耗性能的视图提前渲染。
2、离屏渲染的定义:
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。
总结:离屏渲染就是屏幕外的渲染,渲染的结果不会直接呈现到屏幕上,而是等待合适的时机才会被显示在屏幕上。具体来说,如果要在显示器/屏幕上显示内容,我们至少需要一块与屏幕像素数据量一样大的帧缓存区(frame buffer)来缓存GPU渲染结果。但是有时候会因为一些限制,比如GPU的性能瓶颈或者渲染很复杂的图层(模糊效果、遮罩mask)等,导致无法把渲染结果直接写入到frame buffer,而是先暂存到离屏缓存区(offscreen-buffer),之后再合并处理完的结果一起写入到frame buffer,这个过程叫做离屏渲染。
二、离屏渲染的触发原理
1、离屏渲染存储空间的限制
离屏渲染存储空间是我们屏幕像素的2.5倍。
2、离屏渲染的案例分析:
试例1:app添加遮罩(被动触发)

图中右边的图片3是要显示的最终效果。
那么第一步首先从app将数据(layout等)提交到Core Animation,然后通过Core Animation提交到Render Server,再经过顶点着色器->片元着色器->得到遮罩1 mask texture,但是此时mask texture 不会直接显示再屏幕上,而是被缓存到了离屏缓存区。
第二步,和第一步同样的流程得到了图层2 layer Texture,同样被缓存到了离屏缓存区。
第三步,拿到第一步和第二步的2个离屏缓存区的数据进行合并,然后添加到帧缓存区,最后一起被视频控制器显示在屏幕上。
由上面的流程可见:app在进行额外的渲染和合并的时候,会用到离屏渲染。
试例2:“毛玻璃效果”(被动触发)

首先第一步拿到原图Content缓存到offscreen buffer。
第二步拿到缩放后的图片(如果有缩放)cature content 缓存到offscreen buffer。
第三步拿到水平的模糊缓存到offscreen buffer。
第四部拿到垂直模糊缓存到offscreen buffer。
最后把4个结果合成显示。
**试例3: shouldRasterize 光栅化
如果不可避免的要触发离屏渲染,并且发生渲染试图内容不会频繁变化的时候,可以利用CALayer.shouldRasterize开启关栅化,将离屏渲染的内容以bitmap(位图)的形式缓存,减少复杂试图频繁渲染的开销。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
...
//set text shadow
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
cell.textLabel.layer.shadowOpacity = 0.5;
//rasterize
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}
以上这段代码,每一个cell上面的字符和头像在每一帧刷新的时候并不需要改变,所以看起来UITableViewCell的图层非常适合做缓存。我们可以使用shouldRasterize来缓存图层内容。这将会让图层离屏之后渲染一次然后把结果保存起来,直到下次利用的时候去更新。
备注:光栅化使用建议:
(1)如果layer不能被复用,则没有必要打开光栅化。
(2)如果layer不是静态的,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响了效率。
(3)离屏渲染缓存内容有时间限制,缓存内容100ms时间内如果没有被使用,那么它就会丢弃,无法进行复用了。
(4)离屏渲染缓存空间有限,超过2.5倍屏幕像素大小的话,也会失效,且无法进行复用了。
试例4:圆角触发的离屏渲染
(1)先说一下layer的cornerRadius属性:

以上的view是由背景颜色、边框、和内容组成。
当设置这个view设置cornerRadius的时候,是不会触发离屏渲染的。设置layer.cornerRadius只会设置backgroudColor和border的圆角,不会设置content的圆角,除非同时设置了layer.masksToBounds为true。
备注:iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了(如果同时设置png图片和背景色,依然会触发离屏渲染),如果设置其他阴影效果之类的还是会触发离屏渲染的。
(2)画家算法:

由上图所示,画家算法就是当一个图层进行叠加的时候,先绘制较远的图层,再绘制近的图层。从远到近的依次绘制。
在不使用离屏渲染的时候,当绘制完一个图层(sublayer)会先存到帧缓存区,当显示在屏幕上之后,就会将sublayer从帧缓存区中移除,从而节省空间。
当对这个整个图层进行圆角的时候,会触发离屏幕渲染,此时整个逻辑是:在offscreen buffer中缓存每一个sublayer,然后应用cornerRadius和masksToBounds分别对offscreen buffer中的sublayer进行圆角+裁剪。最后组合之后,显示在屏幕上。
3、离屏渲染为什么会损耗性能
由上面例子1可见,离屏渲染损耗性能的原因有:
(1)离屏渲染需要额外的存储空间
(2)将数据结果从offscreen buffer 转存到 frame buffer(上下文切换) 也需要一定的时间。所以离屏渲染对性能消耗比较大。
4、触发离屏渲染的原因
(1)在渲染特殊效果时,并不能一次性就能得到渲染结果,需要使用额外的offscreen buffer 保存中间状态,不得不使用离屏渲染。如试例1、2、4(系统自动处理,--- 被动触发)
(2)效率的优势:如果有些效果会多次显示在屏幕上,可以提前渲染出结果保存在offscreen buffer里面,达到复用的目的。如试例3(-- 主动触发)
三、常见触发离屏渲染的几种情况
- 使用了 mask 的 layer (layer.mask)
- 需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
- 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/ layer.opacity)
- 添加了投影的 layer (layer.shadow*)
- 采用了光栅化的 layer (layer.shouldRasterize)
- 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
补充:
CPU”离屏渲染“
大家知道,如果我们在UIView中实现了drawRect方法,就算它的函数体内部实际没有代码,系统也会为这个view申请一块内存区域,等待CoreGraphics可能的绘画操作。
对于类似这种“新开一块CGContext来画图“的操作,有很多文章和视频也称之为“离屏渲染”(因为像素数据是暂时存入了CGContext,而不是直接到了frame buffer)。进一步来说,其实所有CPU进行的光栅化操作(如文字渲染、图片解码),都无法直接绘制到由GPU掌管的frame buffer,只能暂时先放在另一块内存之中,说起来都属于“离屏渲染”。
自然我们会认为,因为CPU不擅长做这件事,所以我们需要尽量避免它,就误以为这就是需要避免离屏渲染的原因。但是根据苹果工程师的说法,CPU渲染并非真正意义上的离屏渲染。另一个证据是,如果你的view实现了drawRect,此时打开Xcode调试的“Color offscreen rendered yellow”开关,你会发现这片区域不会被标记为黄色,说明Xcode并不认为这属于离屏渲染。
其实通过CPU渲染就是俗称的“软件渲染”,而真正的离屏渲染发生在GPU。
推荐文章:https://zhuanlan.zhihu.com/p/72653360