概述
离屏渲染(offscreen-rendering)问题是iOS性能优化中需要解决的一个问题,一般来说,少量的离屏渲染其实对我们APP的性能并没有太大影响,但是如果有大量离屏渲染问题,就会导致明显的掉帧和卡顿,离屏渲染就成为了我们APP优化必须解决的一个问题。
本文将从渲染的角度对离屏渲染产生的原因进行阐述。
离屏渲染和在屏渲染(onscreen-rendering)
在理解这这两个概念前,我们先了解iOS渲染的流程。我们通过UIKit设置界面的颜色、位置后,会经由Core Animation,再通过OpenGL ES/Metal绘制图像放到GPU的帧缓冲区中,在下一次垂直信号到来时将帧缓冲区的内容渲染到屏幕上,这就渲染的流程,帧缓冲区的大小也正好等于屏幕像素点的大小。
正常来说,GPU不需要额外开辟一个缓冲空间来进行渲染操作,而这种在当前屏幕缓冲区进行的渲染操作称为在屏渲染。但是在某些情况下,GPU需要额外开辟一块缓冲区进行渲染操作,这种情况称为离屏渲染。
离屏渲染的检测
模拟器:Debug->Color Offscreen-Rendered (离屏渲染的图层高亮成黄,可能存在性能问题)
真机:1.Xcode->Debug->View Debugging->Rendering->Color Offscreen-Rendered Yellow
2.Instrument->Core Animation->Color Offscreen-Rendered Yellow
离屏渲染产生的原因
1.系统触发
通常我们在渲染一个layer的内容时,通常采用油画算法,用先远后近的方式,将一层一层的图层由下至上渲染到屏幕上,正常情况,已经绘制好的内容是可以直接渲染到屏幕上的,并不需要额外的空间保存。但如果一个图层经过着色管道后产生的结果不能马上渲染到屏幕上,而是需要与另一个图层经过管道得到的结果再次进行计算、混合后产生特殊效果才能渲染到屏幕上时,GPU就需要额外开辟一个缓冲区保存这两次中间结果,这个缓冲区就称为离屏缓冲区,这个时候就产生了离屏渲染。
2.手动触发
除了系统触发的离屏渲染,我们也可以通过设置layer的shouldRasterize为YES来触发离屏渲染,在某些场景下,打开 shouldRasterize 可以将一个layer反复利用,从而达到提升效率的优势。
并不是所用的场景下开启光栅化(shouldRasterize)都可以提升效率,如果layer不能被复用,或者layer不是静态,会被频繁修改(比如处于动画之中),那么打开光栅化了反而有可能影响了效率。
另外,离屏渲染缓存内容有时间和空间限制,缓存内容如果100ms内没有被使用,那么它就会被丢弃,如果离屏缓冲区的大小超过2.5倍屏幕大小的话也会失效,无法进行复用。
离屏渲染对性能的影响
相对于在屏渲染,离屏渲染需要开辟一个新的缓冲区,另外离屏渲染在整个过程中需要多次切换上下文环境,如果一帧画面中,存在多个需要的离屏渲染的view,那么GPU的执行时间将会被拉长,如果无法在下一个垂直同步信号到来时完成计算,那么将会产生卡顿,造成性能问题。
iOS离屏渲染产生的场景
通常iOS在以下场景是有可能产生离屏渲染的:
- layer使用了cornerRadius+masksToBounds设置圆角
- layer设置阴影
- layer使用了mask
- layer设置了组透明度
- layer采用了光栅化
- 绘制了文字的layer(UILabel,CATextLayer,Core Text等)
解决layer圆角产生离屏渲染的问题
1.产生的场景
并不是每一个使用了cornerRadius+masksToBounds设置圆角的layer的圆角都会产生离屏渲染,如果一个layer只有一个图层,那么通过cornerRadius+masksToBounds设置圆角,是不会产生离屏渲染的,比如下面两个view:
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 44, 100, 100)];
imgView.image = [UIImage imageNamed:@"img"];
imgView.layer.cornerRadius = 5;
imgView.clipsToBounds = YES;
或者:
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 44, 100, 100)];
imgView.backgroundColor = UIColor.redColor;
imgView.layer.cornerRadius = 5;
imgView.clipsToBounds = YES;
如果一个View只有一个图层,通过cornerRadius+masksToBounds并不会产生离屏渲染,但是如果给例子中的imgView同时设置了背景颜色和图片,或者设置了边框,又或者给imgView添加子View时,就会产生离屏渲染了。
imgView.image = [UIImage imageNamed:@"img"];
imgView.backgroundColor = UIColor.redColor;
imgView.layer.cornerRadius = 5;
imgView.clipsToBounds = YES;
imgView.image = [UIImage imageNamed:@"img"];
imgView.layer.borderWidth = 1.f;
imgView.layer.cornerRadius = 5;
imgView.clipsToBounds = YES;
imgView.image = [UIImage imageNamed:@"img"];
imgView.layer.borderWidth = 1.f;
imgView.layer.cornerRadius = 5;
imgView.clipsToBounds = YES;
UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(120, 44, 40, 40)];
subView.backgroundColor = UIColor.blueColor;
[imgView addSubview:subView];
以上代码均会产生离屏渲染。
2.产生的原因
了解这个问题前,我们需要知道,一个layer是由backgroundColor、contents、border三部分组成的
而从cornerRadius的官方介绍看,单纯设置cornerRadius只能裁剪backgroud和border部分,contents需要陪着masksToBounds才能设置圆角。所以UIImageView图片内容的裁剪,实际上backgroud或者border的mask与其进行混合后产生的结果。
如果layer中只有一个layer的contents,或者只有background和border,那么GPU计算出圆角结果后并不需要与其他图层进行混合或者其他运算,GPU不需要开辟额外缓冲区保存结果,在它渲染完成后就可以将其清除。
但是如果同时设置backgroud和contents,GPU绘制出背景图后,并不能马上将其提交渲染到屏幕上,额需要开辟离屏缓冲区保存背景图,等待GPU绘制完成contents时,在与background内容进行混合运算得到新的图层后,才能渲染到屏幕上,而这个时候,就产生了离屏渲染。
3.解决方案
1.让UI切圆角的图片,省时省力。
2.使用UIBezierPath+CoreGraphics。
imgView.image = [UIImage imageNamed:@"img"];
UIGraphicsBeginImageContextWithOptions(imgView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imgView.bounds cornerRadius:5] addClip];
[imgView drawRect:imgView.bounds];
imgView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
3.使用UIBezierPath+CAShapeLayer
imgView.image = [UIImage imageNamed:@"img"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imgView.bounds cornerRadius:5];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = imgView.bounds;
maskLayer.path = maskPath.CGPath;
imgView.layer.mask = maskLayer;
4.采用三方库如YYImage
YYImage内部实际使用了UIBezierPath+CoreGraphics生成了新的圆角图片。