iOS 离屏渲染探究

为什么要理解离屏渲染

离屏渲染(Offscreen rendering)对iOS开发者来说不是一个陌生的东西,项目中或多或少都会存在离屏渲染,也是面试中经常考察的知识点。一般来说,大多数人都能知道设置圆角、mask、阴影等会触发离屏渲染,但我们深入的探究一下,大家能够很清楚的知道下面几个问题吗?

  • 离屏渲染是在哪一步发生的吗?
  • 离屏渲染产生的原因是什么呢?
  • 设置圆角一定会触发离屏渲染吗?
  • 离屏渲染既然会影响性能我们为什么还要使用呢?优化方案又有那些?

今天我就带着这几个问题探究一下离屏渲染。

ios平台的渲染框架


Core Animation 流水线:


这是在WWDC的Advanced Graphics and Animations for iOS Apps(WWDC14 419)中有这样一张图,我们可以看到,在Application这一层中主要是CPU在操作,而到了Render Server这一层,CoreAnimation会将具体操作转换成发送给GPU的draw calls(以前是call OpenGL ES,现在慢慢转到了Metal),显然CPU和GPU双方同处于一个流水线中,协作完成整个渲染工作。我们也可以把iOS下的Core Animation可以理解为一个复合引擎,主要职责包含:渲染、构建和实现动画。

离屏渲染的定义

  1. OpenGL中,GPU屏幕渲染有以下两种方式当前屏幕渲染(On-Screen Rendering):正常情况下,我们在屏幕上显示都是GPU读取帧缓冲区(Frame Buffer)渲染好的的数据,然后显示在屏幕上。流程如图:

  2. (Off-Screen Rendering ):如果有时因为一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。也就是GPU需要在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。流程如图:

在上面的CoreAnimation流水线示意图中,我们可以得知主要的渲染操作是由CoreAnimation的Render Server模块,通过调用显卡驱动提供的OpenGL或Metal接口执行,对于每一层layer,Render Server会遵循“画家算法”(由远及近),按次序输出到frame buffer,然后按照次序绘制到屏幕,当绘制完一层,就会将该层从帧缓存区中移除(以节省空间)如下图,从左至右依次输出,得到最后的显示结果。


但在某些场景下“画家算法”虽然可以逐层输出,但是无法在某一层渲染完成后,在回过头来擦除/修改某一部分,因为这一层之前的layer像素数据已经被永久覆盖了。这就意味着对于每一层的layer要么能够通过单次遍历就能完成渲染,要么就只能令开辟一块内存作为临时中转区来完成复杂的修改/裁剪等操作。

举例说明:对图3进行圆角和裁剪:imageView.clipsToBounds = YES,imageView.layer.cornerRadius=10时,这就不是简单的图层叠加了,图1,图2,图3渲染完成后,还要进行裁减,而且子视图layer因为父视图有圆角,也需要被裁剪,无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分。所以不能按照正常的流程,因此苹果会先渲染好每一层,存入一个缓冲区中,即离屏缓冲区,然后经过层叠加和处理后,再存储到帧缓存去中,然后绘制到屏幕上,这种处理方式叫做离屏渲染

 常见离屏渲染场景分析

使用Simulator检测项目中触发离屏渲染的图层,如下图:


打开 Color Off-screen Rendered,同时我们可以借助Xcode或 Reveal 清楚的看到那些图层触发了离屏渲染。

关于常见的设置圆角触发离屏渲染示例说明:


如上图示例代码中(btn.png是一个200x300的本地图片),

  • btn1设置了图片,设置了圆角,打开了clipsToBounds = YES,触发了离屏渲染,

  • btn2设置了背景颜色,设置了圆角,打开了clipsToBounds = YES,没有触发离屏渲染,

  • img1设置了图片,设置了圆角,打开了masksToBounds = YES,触发了离屏渲染,

  • img2设置了背景颜色,设置了圆角,打开了masksToBounds = YES,没有触发离屏渲染

解释:btn1和img1触发了离屏渲染,原因是btn1是由它的layer和UIImageView的layer混合起来的效果(UIButton有imageView),所以设置圆角的时候会触发离屏渲染。img1设置cornerRadius和masksToBounds是不会触发离屏渲染的,如果再对img1设置背景色,则会触发离屏渲染。

根据示例可以得出只是控件设置了圆角或(圆角+裁剪)并不会触发离屏渲染,同时需要满足父layer需要裁剪时,子layer也因为父layer设置了圆角也需要被裁剪(即视图contents有内容并发生了多图层被裁剪)时才会触发离屏渲染。

苹果官方文档对于cornerRadius的描述:

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

设置cornerRadius大于0时,只为layer的backgroundColorborder设置圆角;而不会对layer的contents设置圆角,除非同时设置了layer.masksToBoundstrue(对应UIView的clipsToBounds属性)。

圆角触发离屏渲染示意图


一旦我们 为contents设置了内容 ,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染。

其他触发离屏渲染的场景:

  • 采用了光栅化的 layer (layer.shouldRasterize)
  • 使用了 mask 的 layer (layer.mask)
  • 需要进行裁剪的 layer (layer.masksToBounds /view.clipsToBounds)
  • 设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity/ layer.opacity)
  • 使用了高斯模糊
  • 添加了投影的 layer (layer.shadow*)
  • 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)

shouldRasterize 光栅化

shouldRasterize开启后,会将layer作为位图保存下来,下次直接与其他内容进行混合。这个保存的位置就是OffscreenBuffer中。这样下次需要再次渲染的时候,就可以直接拿来使用了。

shouldRasterize使用建议:

  • layer不复用,没必要打开shouldRasterize
  • layer不是静态的,也就是说要频繁的进行修改,没必要使用shouldRasterize
  • 离屏渲染缓存内容有100ms时间限制,超过该时间的内容会被丢弃,进而无法复用
  • 离屏渲染空间是屏幕像素的2.5倍,如果超过也无法复用

离屏渲染的优劣

劣势

离屏渲染增大了系统的负担,会形象App性能。主要表现在以下几个方面:

  • 离屏渲染需要额外的存储空间,渲染空间大小的上限是2.5倍的屏幕像素大小,超过无法使用离屏渲染
  • 容易掉帧:一旦因为离屏渲染导致最终存入帧缓存区的时候,已经超过了16.67ms,则会出现掉帧的情况,造成卡顿

优势

虽然离屏渲染会需要多开辟出新的临时缓存区来存储中间状态,但是对于多次出现在屏幕上的数据,可以提前渲染好,从而进行复用,这样CPU/GPU就不用做一些重复的计算。

特殊产品需求,为实现一些特殊动效果,需要多图层以及离屏缓存区保存中间状态,这种情况下就不得不使用离屏渲染。比如产品需要实现高斯模糊,无论自定义高斯模糊还是调用系统API都会触发离屏渲染。

离屏渲染优化方案(关于实现圆角造成的离屏渲染优化)

方案一

self.view.layer.clipsToBounds = YES;self.view.layer.cornerRadius = 4.f; 

  • clipsToBounds:UIView中的属性,其值主要决定了在视图上的子视图,超出父视图的部分是否截取,默认为NO,即不裁剪子视图超出部分。
  • masksToBounds:CALayer中的属性,其值主要决定了视图的图层上的子图层,超出父图层的部分是否需要裁减掉。默认NO。

方案二

如果产品设计圆角+阴影的卡片,可以使用切图实现圆角+阴影,避免触发离屏渲染

方案三

贝塞尔曲线绘制圆角

- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{

/* 当前UIImage的可见绘制区域 */

CGRect rect = (CGRect){0.f,0.f,size};

/* 创建基于位图的上下文 */

UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);

/* 在当前位图上下文添加圆角绘制路径 */

CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);

/* 当前绘制路径和原绘制路径相交得到最终裁剪绘制路径 */

CGContextClip(UIGraphicsGetCurrentContext());

/* 绘制 */

[self drawInRect:rect];

/* 取得裁剪后的image */

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

/* 关闭当前位图上下文 */

UIGraphicsEndImageContext();

return image;

}

方案四

CAShapeLayer + UIBezierPath 绘制圆角来实现UITableViewCell圆角并绘制边框颜色(这种方式比直接设置圆角方式好,但也会触发离屏渲染),代码如下:

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{

CAShapeLayer *maskLayer = [CAShapeLayer layer];

maskLayer.frame = CGRectMake(0, 0, cell.width, cell.height);

CAShapeLayer *borderLayer = [CAShapeLayer layer];

borderLayer.frame = CGRectMake(0, 0, cell.width, cell.height);

borderLayer.lineWidth = 1.f;

borderLayer.strokeColor = COLOR_LINE.CGColor;

borderLayer.fillColor = [UIColor clearColor].CGColor;

UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, cell.width, cell.height) cornerRadius:kRadiusCard];

maskLayer.path = bezierPath.CGPath;

borderLayer.path = bezierPath.CGPath;

[cell.contentView.layer insertSublayer:borderLayer atIndex:0];

[cell.layer setMask:maskLayer];

}

YYKit是开发中经常用的三方库,YYImage对图片圆角的处理方法是值得推荐的,附上实现源码:

- (UIImage *)imageByRoundCornerRadius:(CGFloat)radius

corners:(UIRectCorner)corners

borderWidth:(CGFloat)borderWidth

borderColor:(UIColor *)borderColor

borderLineJoin:(CGLineJoin)borderLineJoin {

if (corners != UIRectCornerAllCorners) {

UIRectCorner tmp = 0;

if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;

if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;

if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;

if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;

corners = tmp;

}

UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);

CGContextRef context = UIGraphicsGetCurrentContext();

CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);

CGContextScaleCTM(context, 1, -1);

CGContextTranslateCTM(context, 0, -rect.size.height);

CGFloat minSize = MIN(self.size.width, self.size.height);

if (borderWidth < minSize / 2) {

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];

[path closePath];

CGContextSaveGState(context);

[path addClip];

CGContextDrawImage(context, rect, self.CGImage);

CGContextRestoreGState(context);

}

if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) {

CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;

CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);

CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, borderWidth)];

[path closePath];

path.lineWidth = borderWidth;

path.lineJoinStyle = borderLineJoin;

[borderColor setStroke];

[path stroke];

}

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

return image;

}

拓展阅读:

iOS圆角的离屏渲染,你真的弄明白了吗:关于圆角触发离屏渲染更详细的分析

关于iOS离屏渲染的深入研究:即刻技术团队对离屏渲染的解析



以上是 iOS 离屏渲染探究 的全部内容, 来源链接: utcz.com/a/31078.html

回到顶部