离屏渲染的探究
什么是离屏渲染
作为一个 iOS 开发者,你肯定听说过离屏渲染,什么是离屏渲染呢?
案例
在模拟器上显示一张裁剪的圆角图片
UIImageView *bgImageView = [[UIImageView alloc] initWithFrame:CGRectMake(107, 100, 200, 200)];
bgImageView.backgroundColor = [UIColor whiteColor];
bgImageView.image = [UIImage imageNamed:@"qh"];
bgImageView.layer.cornerRadius = 100.0f;
bgImageView.layer.masksToBounds = YES;
[self.view addSubview:bgImageView];
开启模拟器 debug 模式下的离屏渲染
运行模拟器,会发现此时的 imageView 变成了黄色,说明出现了离屏渲染。
修改一下代码,去掉 backgroundColor,
// bgImageView.backgroundColor = [UIColor whiteColor];
bgImageView.image = [UIImage imageNamed:@"qh"];
bgImageView.layer.cornerRadius = 100.0f;
bgImageView.layer.masksToBounds = YES;
运行效果图
黄色部分消失,说明设置了 cornerRadius + masksToBounds 不一定会导致离屏渲染。
离屏渲染过程中,渲染显示发生了什么?
正常情况下,App 通过 CPU 和 GPU 的合作,不停的将内容渲染完成放入 Frame Buffer 帧缓冲器中,而显示屏幕不断的从 Frame Buffer 中获取内容,显示实时的内容。
离屏渲染的流程:先额外创建离屏渲染缓冲区 Offscreen Buffer,将提前渲染好的内容放入 Offscreen Buffer,等到合适的时机再将 Offscreen Buffer 中的内容进一步的叠加、渲染、完成后将结果切换到 Frame Buffer 中,按照正常的模式继续下去。
为什么在帧缓存区之前多了离屏渲染?
如果渲染的画面比较复杂,如 UIImageView 的背景色 backgroundColor 需要裁剪渲染,image 也需要裁剪渲染,正常情况下,根据画家算法(由远及近的显示),先显示远处的背景,再显示近处的背景:
backgroundColor 渲染之后的位图先进入 帧缓冲区->屏幕,帧缓冲区的backgroundColor 清空;
image 的位图再进入 帧缓冲区->屏幕 ,帧缓冲区的 image 被清空;
再对 image 进行裁剪时,因为帧缓冲区已经被清空了,已经没有东西裁剪了。
因此,需要额外开辟一块缓冲区,等待合成、裁剪完成后-->帧缓冲区-->屏幕显示。 那么,这个额外的处理复杂渲染数据的地方就是 离屏渲染缓冲区(Offscreen Buffer)。 所以,在帧缓冲区之前要多了一个离屏渲染缓冲区。
离屏渲染对性能的影响
离屏渲染会加大系统的负担,会造成性能上的损耗,主要表现在:
-
Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。
-
离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间,则会出现掉帧的情况。
离屏渲染的开销很大,为什么还要使用离屏渲染呢?
-
对于一些特殊效果(一般都是系统自动触发的,比如阴影、圆角、遮罩、光栅化等)需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
-
处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
常见的触发离屏渲染的几种情况
- 使用了 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 等)
导致离屏渲染的解决办法
iOS 9.0 之前 UIImageView 跟 UIButton 设置圆角都会触发离屏渲染。
iOS 9.0 之后 UIButton 设置圆角会触发离屏渲染,而 UIImageView 里 png 图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。
圆角优化
我们设置圆角一般通过如下方式:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(107, 100, 200, 200)];
imageView.image = [UIImage imageNamed:@"qh"];
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;
[self.view addSubview:imageView];
这样处理的渲染机制是GPU在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会给我们带来额外的性能损耗,如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的频繁切换,性能的代价会宏观地表现在用户体验上——掉帧。
优化方案1:使用贝塞尔曲线 UIBezierPath 和 Core Graphics 框架画出一个圆角
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(107, 100, 200, 200)];
imageView.image = [UIImage imageNamed:@"qh"];
//开始对 imageView 进行画图
UIGraphicsBeginImageContextWithOptions(imageView.frame.size, NO, [UIScreen mainScreen].scale);
//使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width / 2] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
//结束画图
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
优化方案2:使用 CAShapeLayer 和 UIBezierPath 设置圆角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(107, 100, 200, 200)];
imageView.image = [UIImage imageNamed:@"qh"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//设置大小
maskLayer.frame = imageView.bounds;
//设置图形样子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
[self.view addSubview:imageView];
⚠️CAShapeLayer 需要贝塞尔曲线配合使用才有意义(才有效果),CAShapeLayer 继承于 CALayer,可以使用 CALayer 的所有属性值。CAShapeLayer 动画渲染直接提交到手机的 GPU 当中,相较于 view 的drawRect 方法使用 CPU 渲染而言,其效率极高,能大大优化内存使用情况。
优化方案3:使用带圆角的图片
- 直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。但是这种方法需要依赖具体情况,并不通用。
优化方案4:添加遮罩
- 再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
离屏渲染的另外一个原因 - 光栅化
官方说明:
/* When true, the layer is rendered as a bitmap in its local coordinate
* space ("rasterized"), then the bitmap is composited into the
* destination (with the minificationFilter and magnificationFilter
* properties of the layer applied if the bitmap needs scaling).
* Rasterization occurs after the layer's filters and shadow effects
* are applied, but before the opacity modulation. As an implementation
* detail the rendering engine may attempt to cache and reuse the
* bitmap from one frame to the next. (Whether it does or not will have
* no affect on the rendered output.)
*
* When false the layer is composited directly into the destination
* whenever possible (however, certain features of the compositing
* model may force rasterization, e.g. adding filters).
*
* Defaults to NO. Animatable. */
使用光栅化建议:
-
如果layer不能被重用,则没必要使用光栅化;
-
如果我们更新已光栅化的layer,会造成大量的离屏渲染。例如UITableViewCell因为复用的原因,重绘是很频繁的。如果此时设置了光栅化,反而会造成大量离屏渲染,降低性能;
-
离屏渲染的缓存是有时间限制的,100ms内如果缓存的内容没有被复用,则会被丢弃,也就无法复用了;
-
离屏渲染的空间有限,超过2.5倍屏幕像素的大小,离屏渲染也会失效,无法复用。