关于iOS的性能优化,我们经常会听到“离屏渲染”这个词,而对于离屏渲染的了解很多人只停留在设置圆角会导致这个问题。那么什么是离屏渲染?为何会造成离屏渲染?怎么解决离屏渲染的问题?这些是本节的主要内容。
既然我们知道圆角会造成离屏渲染,那么先来写一个demo。
@interface ViewController () @property (nonatomic, strong) UIView *blackRoundView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.blackRoundView = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; self.blackRoundView.backgroundColor = [UIColor blackColor]; self.blackRoundView.layer.cornerRadius = 50.f; self.blackRoundView.layer.masksToBounds = YES; [self.view addSubview:self.blackRoundView]; }
运行后打开模拟器的Debug→Color Offscreen-Rendered,可以发现并没有任何有离屏渲染的地方。
这是为什么呢?可以先看一下苹果的官方文档,在其文档中对于圆角属性cornerRadius的解释是这样的:
给layer设置一个大于0的半径值会给其背景绘制一个圆角,默认情况下,圆角半径不会对layer的contents属性(通常为CGImage)造成影响,而是对layer的背景以及边框(border)有影响,所以如果要使layer的contents也带有圆角效果,需要设置layer的masksToBounds属性为YES。
通过上面这一段官方文档,我们了解到在设置圆角时,如果仅给一个圆角值是没有效果的,还需要配上masksToBounds属性设置为YES才能达到我们想要的效果。同时也提到了关于CALayer的结构的介绍,官方文档中还给出了一幅关于CALayer结构的示例图,如图所示。
从图中可以看到CALayer是由三部分组成的,分别是background、contents和border。在给CALayer设置圆角时,刚刚提到只会给background和border设置圆角,而不会对contents设置圆角,但对于layer来说,在屏幕上显示的是其contents,所以如果不设置masksToBounds为YES的话,并不能看到其真正圆角效果。
同样这也解释了我们一开始的demo中并没有触发离屏渲染的原因是由于我们只是设置了UIView的背景,并没有设置其contents,因而没有触发离屏渲染。但在实际开发中,我们遇到的问题大多数还是对于UIImageView的圆角操作,例如UITableViewCell中一大堆的圆角头像等。在UIImageView中,设置image即可以显示在屏幕上,这是因为,UIImage其实是对CGImage的封装,其显示还是根据CGImage的内容来决定,对于imageView的layer的contents来说,同样也是需要一个CGImage对象,因此我们在给UIImageView赋值UIImage对象时会将UIImage的CGImage赋给imageView.layer.contents,这样imageView最终才显示出了图片。那如果此时我们对imageView设置cornerRadius和masksToBounds就发生了离屏渲染。
那么到底什么是离屏渲染呢?
当一个layer只做基本设置时,也就是不做触发离屏渲染的操作,是可以直接放入缓冲区中供GPU来渲染到屏幕上的。但当你设置圆角、遮罩和阴影后,layer的帧绘制不能被直接放到GPU读取帧的那个缓冲区了,也就无法直接渲染到屏幕上,因此需要另外再创建一个新的缓冲区,并将需要绘制的图层在两个缓冲区中的上下文进行切换,然而这种切换是非常昂贵的,如果切换上下文不及时或者渲染操作过长就会导致离屏渲染的卡顿和性能问题。而GPU直接读取帧渲染到屏幕上的缓冲区就是屏幕内渲染,在当前屏幕缓冲区之外所创建新的缓冲区就是离屏渲染。
是否还记得高中物理中电视机的原理,是通过电子枪一行一行发射电子到屏幕上形成的,每当电子枪另起一行时,会发出一个水平同步信号,当电子枪扫描到屏幕最后一个点时,表示一帧已经绘制结束,会发出一个垂直同步的信号,并再回到起点开始下一帧扫描。手机屏幕显示的原理也是类似的,当GPU从缓冲区中取出一帧渲染到屏幕结束后,会发出一个垂直同步信号,这时会去缓冲区取下一帧,如果没有取到,说明GPU渲染没有完成,于是放弃下一次渲染,此时屏幕停留在原来的显示位置不动,对用户来说就形成了卡顿效果。
上面提到,屏幕内渲染和离屏渲染会有两个缓冲区,这两个缓冲区都是由GPU来执行渲染操作的。但值得注意的是,还存在一种离屏渲染,是由drawRect方法引起的离屏渲染,这是由CPU来负责处理的。在drawRect方法中,一般使用CoreGraphics来绘制一些图形和文字,那么系统在处理这部分时会交给CPU来负责计算和渲染,渲染结束再交付给GPU显示,注意CPU绘制的操作是同步的。
虽然GPU相对于CPU来说,更擅长处理界面渲染的这些操作,但是对于离屏渲染中切换上下文的操作是非常昂贵的,在上下文中需要冲破对传递管道和一些障碍的限制。鉴于此,我们可以考虑用CPU去执行一些操作来帮助GPU分担任务。那到底如何分担呢?对于比较简单的触发离屏渲染的界面,可以使用Core Graphics在drawRect中绘制,虽然都仍然是离屏渲染,但相对来说要比让GPU做上下文切换要好一些。如果界面稍显复杂,用drawRect来说或许仍然可以起到一点儿效果,但还可以通过设置光栅来进行优化。
对于光栅化来说,虽然是触发离屏渲染的条件之一,但是对于开发者来说,可以看作是对离屏渲染问题的一个优化措施,但光栅化使用是有一定的条件。不同于圆角、遮罩、阴影、边界反锯齿以及设置组不透明等触发离屏渲染的条件,光栅化是CPU对绘制内容的图层生成位图进行缓存一段很短的时间,如果在该时间内需要再次渲染该图层,可以直接通过该位图缓存来渲染,不需要再次计算和上下文的切换,但是这仅限于对于相同的图层,例如,UITableViewCell中的一个相同的圆形icon就可以使用光栅化来进行优化,但假设是UITableViewCell复用中的变化着的头像,则使用光栅化并不能起到优化的左右,因为生成的位图缓存不仅没用上,还需要花时间去生成位图缓存,这样反而使性能更低了。
对于离屏渲染,一开始是苹果提出的一种GPU任务处理的方案,但是现在却逐渐成为一种性能杀手,作为开发者,不可忽视离屏渲染带来的问题,应对其进行合理的优化或者避免触发。那么如何去解决离屏渲染产生的性能问题呢?
上文说到,如果实在没有办法避免离屏渲染,可以采用:①drawRect方法使CPU来分担GPU压力;②使用光栅来对图层进行短时间缓存。那如果需要避免离屏渲染的问题该如何处理?
在现在iOS开发中比较流行的解决办法是对图片进行重新绘制,将原图利用CoreGraphics来重新绘制一个带有圆角的新图片,这样便不用设置图层的圆角属性即可达到效果,这样一来确实解决了离屏渲染带来的性能问题,但随之而来又产生了一个新的问题,便是绘制是无法避免的,特别是对于UITableViewCell中的圆角实现,每次复用都需要重绘一次,这对CPU产生了不少的压力,如果涉及的圆角过多,则可能会产生更大的性能问题。对于该问题,有开发者认为可以对生成的重绘图进行缓存,但为此还需要写一套关于缓存的逻辑,并且如果缓存的新图片个数过多,则还会带来内存压力。因此可以采用在涉及圆角图片的UIImageView上再增加一个UIImageView,该UIImageView是带有一个透明圆角中心的图片,覆盖在原有图片之上,起到一个手工遮罩的作用,当然在iOS 8之后,可以通过设置UIView的maskView属性来达到同样的效果。看起来虽然显得有些low,但相对于离屏渲染和重绘带来的内存压力来说效果是显而易见的,并且不用考虑关于UITableViewCell复用的问题。
本节小结
(1)了解APP离屏渲染的概念,触发条件以及对性能的影响;
(2)与离屏渲染相对应的还有一个概念:屏幕内渲染,以及CPU与GPU在图形渲染上的机制,最后需要对比并应用几种离屏渲染的优化措施。