Offscreen Rendering
如何检测你的项目中是否 触发了离屏渲染问题
那么为何有一些会触发离屏渲染,而有一些却不会触发呢?下面我们开始深入的探索。
离屏渲染的具体过程
我们知道通常的渲染流程是这样的:
App通过CPU和GPU的合作,不停的将内容渲染完成放入FrameBuffer帧缓存区,而屏幕显示不断从FrameBuffer中获取内容,显示实时的内容。
但是离屏渲染的流程是这样的:
在普通的情况下,GPU直接将渲染好的内容放入FrameBuffer中,但是在离屏渲染时不同,需要先额外创建离屏渲染缓存区OffscreenBuffer。将提前渲染好的内容放入其中,等到合适的时机再将OffSreeBuffer中的内容进一步叠加、渲染。完成后将结果切换到FrameBuffer中。
离屏渲染的效率问题
从上面的流程来看,离屏渲染时,由于App需要提前对部分内容进行额外的渲染并保存到OffScreenBuffer,以及需要在必要时对OffScreenBuffer和FrameBuffer进行内容切换,所以会需要更长的处理时间。(实际上这两步切换的代价是非常大的)。
OffScreenBuffer本身就需要额外的空间,大量的离屏渲染可能造成内存过大的压力。与此同时,OffScreenBuffer的总大小也是有限的:不能超过屏幕总像素的2.5倍。
可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,就容易造成掉帧问题,所以大部分情况下,我们要避免出现离屏渲染。
为什么要用离屏渲染
既然离屏渲染会造成性能问题,那么为什么还要使用离屏渲染?
其实主要是以下两种原因:
- 一些特殊的效果需要使用额外的OffScreenBuffer来保存渲染中间的状态,所以不得不使用离屏渲染
- 出于效率的目的,可以将内容提前渲染并保存到OffScreenBuffer中,从而达到复用的目的
例如,第一种原因,也就是不得不使用离屏渲染的情况。一般都是系统自动触发。如:阴影、圆角等。比如我们使用的蒙版(mask)功能,因为最终的结果是有超过一层的渲染结果进行叠加,所以必须要利用额外的内存空间对中间的渲染结果进行保存,因此系统会默认触发离屏渲染.
比如,iOS8开始提供模糊特效UIBlurEffectView:
- 先渲染需要模糊的内容本身;
- 对内容进行缩放;
- 对上一步结果进行垂直模糊;
- 对上一步结果进行横向模糊;
- 最后一步,将模糊后的结果进行叠加合成,实现最终完整的模糊效果。
在这样的5次过程,系统也会自动触发离屏渲染,用来保存复杂的特效下,利用额外的内存空间对中间的结果进行保存,以便最后进行效果的合成。
离屏渲染的第二种原因:shouldRasterize 光栅化
开启光栅化后,就会主动触发离屏渲染。Render Server会强制将CALayer渲染位图结果bitmap保存下来,这样下次渲染可以直接复用,提高效率;而保存下来的bitmap就已经包含了layer和sublayer、圆角、阴影、透明度等。
如果layer的构成包含了以上几种元素,结构非常复杂且还需要重复利用,可以考虑开启光栅化;因为layer 上的圆角、阴影、透明度等会由系统自动触发离屏渲染,那么打开光栅化就可以节约第二次以及以后的渲染时间。
而多层的subLayer的情况由于不会自动触发离屏渲染,所以相比之下会花费第一次离屏渲染的时间,但是可以节约后续重复的渲染开销。
使用光栅化的注意点:
- 如果layer并不能被复用,则没必要开启;
- 如果layer不是静态的,需要 被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率了;
- 离屏渲染缓存内容有时间限制,缓存内容如果100ms没被复用,那么就会被丢弃,无法进行复用;
- 离屏渲染缓存空间有限,超过2.5倍屏幕像素大小的话,也会失效,且无法复用
圆角的离屏渲染探索
通常来讲,设置了Layer的圆角效果后,会自动触发离屏渲染,但是具体什么情况下设置圆角才会触发离屏渲染?
如上图所示,Layer由3层组成,我们设置圆角通常是用下面的代码:
<pre data-tool="mdnice编辑器" style="margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; border-top-left-radius: 5px; border-top-right-radius: 5px; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; box-sizing: border-box !important; word-wrap: break-word !important;">view.layer.cornerRadius = 2;
</pre>
CornerRadius - Apple 官方介绍
根据苹果的描述,上面这句代码,只会默认设置backgroundColor和border的圆角,而不会设置content的圆角,除非设置了layer.maskToBounds为true(对应view的clickToBounds属性)。
如果只设置了CornerRadius而没有设置masksToBounds,由于不需要叠加裁剪,此时是不会触发离屏渲染的。而当设置了裁剪属性时,由于maskToBounds会对layer以及所有 的subLayer的content都进行裁剪,所以不得不触发离屏渲染。
<pre data-tool="mdnice编辑器" style="margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; border-top-left-radius: 5px; border-top-right-radius: 5px; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; box-sizing: border-box !important; word-wrap: break-word !important;">view.layer.masksToBounds = true //触发离屏渲染的原因
</pre>
离屏渲染的逻辑
刚才说圆角加上masksToBounds时,因为masksToBounds会对layer上的所有内容进行裁剪,从而诱发了离屏渲染,那么这个过程具体是怎么回事呢?我们来仔细研究一下:
- 在普通的layer绘制中,上层的subLayer会覆盖下层的subLayer,下层的subLayer在绘制完成后就可以抛弃了,从而节约空间提高效率。
- 所有subLayer一次绘制完毕后,整个绘制过程完成,就可以进行后续的呈现了。假设我们需要绘制一个三层的subLayer,并不设置裁剪和圆角,那么整个绘制过程就如下图所示:
绘制完进行display
设置了CornerRadius以及masksToBounds
当我们设置了CornerRadius以及masksToBounds进行圆角加裁剪时,masksToBounds裁剪属性会应用到所有的subLayer上,也就意味着所有的subLayer都要进行圆角+裁剪,意味着所有的subLayer在第一次绘制后,并不能立刻丢弃,而必须保存在OffScreenBuffer中等待下一轮的圆角加裁剪操作,这样便引发了离屏渲染。
实际上,并不单只有圆角加裁剪会触发离屏渲染。如果设置了透明度和组透明(layer.allowsGroupOpacity+layer.opacity),阴影属性(shadowOffset)等,都会产生这样的离屏渲染,因为这些都不是对单一的layer进行处理,而是对layer及其所有 的subLayer进行处理,从而引发离屏渲染。
避免圆角离屏渲染的手段:
除了尽量减少圆角裁剪的使用,还有什么别的办法可以避免圆角+裁剪引起的离屏渲染?
由于刚才提到,圆角引起离屏渲染的本质是裁剪的叠加,导致了masksToBounds对layer及其所有的subLayer进行了二次处理,那么我们只要避免使用masksToBounds进行二次处理,而是对所有的subLayer进行预处理,就可以只进行“画家算法(先绘制离屏幕较远的图层,然后绘制距离屏幕较近的图层,根据深度值,确定绘制顺序)”,用一次叠加就完成绘制。
有哪些可行方案
1.换资源 直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这周方法需要依赖具体情况,并不通用 。
2.mask 再增加一个和背景色相同的遮罩mask覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片 或渐变色的情况。注意这里的mask并不是指的layer上的mask,而是用两个view的叠加
3.UIBezierPath 用贝塞尔曲线绘制闭合带圆角的矩形,在上下文设置只有内部可见,再将不带圆角的layer渲染成图片,添加到贝塞尔矩形中。这种方法效率较高,但是layer的布局一旦改变,贝塞尔曲线都需要手动进行重新绘制,所以需要对frame、color等进行手动监听并重绘。
4.CoreGraphics 重写 drawRect:,用coreGraphics相关方法,在需要应用圆角时进行手动绘制。不过CoreGraphics效率也有限,如果多次调用也会有效率问题。
触发离屏渲染的几种情况
- 使用了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,CoreText等)
举几个例子,加深下理解:
//按钮存在背景图片 因为 会对button的layer和其imageView的layer进行圆角加裁剪 所以会触发离屏渲染
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 30, 100, 100);
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
[self.view addSubview:btn1];
btn1.layer.cornerRadius = 50;
btn1.clipsToBounds = YES;
//按钮存在背景图片 只设置了clipsToBounds,裁剪是针对于imageview的,所以不会触发离屏渲染(not offscreen rendering)
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 30, 100, 100);
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
[self.view addSubview:btn1];
btn1.imageView.layer.cornerRadius = 50;
btn1.clipsToBounds = YES;
//按钮不存在背景图片 虽然设置了圆角+裁剪,但是只有一层layer 所以不会触发离屏渲染
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, 180, 100, 100);
btn2.backgroundColor = [UIColor blueColor];
[self.view addSubview:btn2];
btn2.layer.cornerRadius = 50;
btn2.clipsToBounds = YES;
//UIImageView 设置了图片+背景色; 背景色和图片两层layer都需要进行圆角+裁剪 所以会触发离屏渲染
UIImageView *img1 = [[UIImageView alloc]init];
img1.frame = CGRectMake(100, 320, 100, 100);
img1.backgroundColor = [UIColor blueColor];
img1.image = [UIImage imageNamed:@"btn.png"];
[self.view addSubview:img1];
img1.layer.cornerRadius = 50;
img1.layer.masksToBounds = YES;
//UIImageView 只设置了图片,无背景色; 只有一层 layer 需要圆角+裁剪 所以不会触发离屏渲染
UIImageView *img2 = [[UIImageView alloc]init];
img2.frame = CGRectMake(100, 480, 100, 100);
img2.image = [UIImage imageNamed:@"btn.png"];
[self.view addSubview:img2];
img2.layer.cornerRadius = 50;
img2.layer.masksToBounds = YES;