什么是离屏渲染
APP的渲染流程是CPU将图片解码 -> frame buffer(帧缓冲区) -> 视频控制器读取显示,然后把图片丢掉。如下图:
但是在某些情况下无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域Offscreen Buffer,之后再写入frame buffer,那么这个过程被称之为离屏渲染。如下图:
iOS中渲染的底层是由OpenGL/Metal来完成的,在OpenGL/Metal中有一种渲染方法是油画渲染,就是在渲染过程中根据深度值由大到小进行渲染,也就是说先渲染远处的图像,由远到近依次渲染,可以根据此图来感受一下:先绘制红⾊部分,再绘制⻩⾊部分,最后再绘制灰⾊部分。
但是如果我们需要将整个图像处理一下,比如:圆角、加一层遮罩、加阴影或者我们需要多次用到整个图像,那我们需要将渲染完的整个图像存放到一个缓冲区(Offscreen Buffer)中,然后在帧缓冲区中进行整个图像的处理(圆角等操作),最后显示到屏幕上。
如何才会触发离屏渲染
1.圆角
开启圆角都是这两行代码button.clipsToBounds = YES;button.layer.cornerRadius = 4;
通常情况下设置圆角就会触发离屏渲染,但是设置圆角并不是100%的触发离屏渲染,比如一个button 我只是设置了背景色和字体,开启圆角设置,在这种情况下会触发离屏渲染么?
带着这个疑问让我们来探究一下代码:
//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;
//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;
//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"];
//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"];
在模拟器中运行是这样的
然后我们在debug中打开color Offscree-Renderd
打开之后我们就会发现模拟器中有些图片被标记了黄色,那这些标记黄色的图片就开启了离屏渲染,1和3开启了离屏渲染。在这四段代码中都开启了圆角设置,那为什么只有1、3是离屏渲染呢?
在iOS中CALayer由backgroundColor、contents、borderWidth&borderColor构成。
cornerRadius只对backgroundColor和borderWidth&borderColor生效,如果contents不为空,只有设置masksToBounds为 Yes,contents才会变成圆角,此时是圆角操作可以看作两个部分渲染,并且同时需要显示到屏幕上,就会触发离屏渲染,所以代码1、3触发了离屏渲染
圆角如何才能避免离屏渲染呢
- 使用带有圆角图片;
- 预先使用CoreGraphics为图片裁剪圆角;
- 使用不透明的带有四个圆角图层
2. 阴影(shadow)
其原因在于,虽然layer本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。但矛盾在于此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去。
阴影如何避免离屏渲染
通过CoreAnimation的shadowPath属性,记录阴影的几何形状,那么阴影就可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。
view.layer.shadowPath=[UIBezierPath pathWithCGRect:view.bounds].CGPath;
3. 组透明度(group opacit)
GroupOpacity 是指 CALayer 的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性。开启 GroupOpacity 后,子 layer 在视觉上的透明度的上限是其父 layer 的opacity。
从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。
GroupOpacity 开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。
这个触发条件并不需要subLayer.opacity != 1.0,非常容易满足。然而在 TableView 这样的视图里设置 cell 或 cell.contentView 的alpha属性小于1并不能检测离屏渲染的黄色特征,性能上也没有明显差别。经过摸索发现:只有设置 tableView 的alpha小于1时才会触发离屏渲染,对性能无明显影响;设置 cell 的alpha属性并不会对整体的透明度产生影响,只有设置 cell.contentView 才有效。
在一般的 UIViewController 的视图下可以很容易地观察到 GroupOpacity 触发的离屏渲染,这里只能猜测 TableView 更改了这些行为。
4.遮罩(Masking)
mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,那么其实和group opacity的原理类似,不得不在离屏渲染中完成。
5.光栅化(shouldRasterize)
光栅化是手动启动离屏渲染。
shouldRasterize = false时,离屏渲染的黄色特征仅限于自动触发离屏渲染的效果的部分;shouldRasterize = true后该部分和开启了该属性的 layer 整体都有黄色特征。
光栅化的使用建议
- 如果layer不需要被复用,则不需要打开;
- 如果layer不是静态的,需要被频繁修改,比如出于动画之中,则开启光栅华反而影响性能
- 离屏渲染缓存有时间限制,当超过100ms,内容没有被使用就会被丢弃,无法复用
- 离屏渲染缓存有空间限制,超过屏幕像素的2.5倍则失效,并无法使用
离屏渲染的利弊
优点:
- 提高渲染效率。比如说某种效果多次出现在屏幕上,利用离屏渲染机制进行复用。
缺点
- 增大了性能的损耗。
- 容易掉帧。
离屏渲染本来是个优化设计,但是在优化同时也牺牲了性能,具体的是否启用离屏渲染,还是需要具体的需求来判断。