离屏渲染(Offscreen rendering)对iOS开发者来讲不是一个陌生的东西,项目中或多或少都会存在离屏渲染,也是面试中常常考察的知识点。通常来讲,大多数人都能知道设置圆角、mask、阴影等会触发离屏渲染,但咱们深刻的探究一下,你们可以很清楚的知道下面几个问题吗?ios
今天我就带着这几个问题探究一下离屏渲染。面试
这是在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能够理解为一个复合引擎,主要职责包含:渲染、构建和实现动画。算法
但在某些场景下“画家算法”虽然能够逐层输出,可是没法在某一层渲染完成后,在回过头来擦除/修改某一部分,由于这一层以前的layer像素数据已经被永久覆盖了。这就意味着对于每一层的layer要么可以经过单次遍历就能完成渲染,要么就只能令开辟一块内存做为临时中转区来完成复杂的修改/裁剪等操做。缓存
举例说明: 对图3进行圆角和裁剪:imageView.clipsToBounds = YES,imageView.layer.cornerRadius=10时,这就不是简单的图层叠加了, 图1,图2,图3渲染完成后, 还要进行裁减,并且 子视图layer由于父视图有圆角,也须要被裁剪, 没法在某一层渲染完成以后,再回过头来擦除/改变其中的某个部分。因此不能按照正常的流程,所以苹果会先渲染好每一层,存入一个缓冲区中,即 离屏缓冲区 ,而后通过层叠加和处理后,再存储到帧缓存去中,而后绘制到屏幕上,这种处理方式叫作 离屏渲染
使用Simulator检测项目中触发离屏渲染的图层,以下图:bash
打开 Color Off-screen Rendered,同时咱们能够借助Xcode或 Reveal 清楚的看到那些图层触发了离屏渲染。并发
关于常见的设置圆角触发离屏渲染示例说明:app
如上图示例代码中(btn.png是一个200x300的本地图片),框架
解释:btn1和img1触发了离屏渲染,缘由是btn1是由它的layer和UIImageView的layer混合起来的效果(UIButton有imageView),因此设置圆角的时候会触发离屏渲染。img1设置cornerRadius和masksToBounds是不会触发离屏渲染的,若是再对img1设置背景色,则会触发离屏渲染。
根据示例能够得出只是控件设置了圆角或(圆角+裁剪)并不会触发离屏渲染,同时须要知足父layer须要裁剪时,子layer也由于父layer设置了圆角也须要被裁剪(即视图contents有内容并发生了多图层被裁剪)时才会触发离屏渲染。post
苹果官方文档对于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’scontents
property; it applies only to the background color and border of the layer. However, setting themasksToBounds
property totrue
causes the content to be clipped to the rounded corners.
设置cornerRadius
大于0时,只为layer的backgroundColor
和border
设置圆角;而不会对layer的contents
设置圆角,除非同时设置了layer.masksToBounds
为true
(对应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开启后,会将layer做为位图保存下来,下次直接与其余内容进行混合。这个保存的位置就是OffscreenBuffer中。这样下次须要再次渲染的时候,就能够直接拿来使用了。
shouldRasterize使用建议:
离屏渲染增大了系统的负担,会形象App性能。主要表如今如下几个方面:
虽然离屏渲染会须要多开辟出新的临时缓存区来存储中间状态,可是对于屡次出如今屏幕上的数据,能够提早渲染好,从而进行复用,这样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离屏渲染的深刻研究:即刻技术团队对离屏渲染的解析