iOS离屏渲染探究

朋学良

iOS离屏渲染探究

GPU渲染机制

CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

--

一、GPU屏幕渲染方式:

  • On-Screen Rendering

意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

  • Off-Screen Rendering

意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

  • 特殊的离屏渲染

如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式: CPU渲染。 如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内 同步地完成,渲染得到的bitmap最后再交由GPU用于显示。

- (void)display {
   dispatch_async(backgroundQueue, ^{
       CGContextRef ctx = CGBitmapContextCreate(...);
       CGImageRef img = CGBitmapContextCreateImage(ctx);
       CFRelease(ctx);
       dispatch_async(mainQueue, ^{
           layer.contents = img;
       });
   });
}

备注:CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程

二、离屏渲染的触发方式

设置了以下属性时,都会触发离屏绘制:

  • shouldRasterize(光栅化)
  • masks(遮罩)
  • shadows(阴影)
  • edge antialiasing(抗锯齿)
  • group opacity(不透明)
  • 复杂形状设置圆角等

--

1、shouldRasterize(光栅化)

光栅化概念:将图转化为一个个栅格组成的图象。 光栅化特点:每个元素对应帧缓冲区中的一像素。

shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度

当你使用光栅化时,你可以开启“Color Hits Green and Misses Red”来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。

如果光栅化的层变红得太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁,红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。

  • 我们经常的TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化。则会造成大量的离屏渲染,降低图形性能。
  • 有时候我们可以把那些需要屏幕外绘制的图层开启光栅化以作为一个优化方式,前提是这些图层并不会被频繁地重绘。

--

2、masks(遮罩)

mask是layer的一个属性.

/* A layer whose alpha channel is used as a mask to select between the
 * layer's background and the result of compositing the layer's
 * contents with its filtered background. Defaults to nil. When used as
 * a mask the layer's `compositingFilter' and `backgroundFilters'
 * properties are ignored. When setting the mask to a new layer, the
 * new layer must have a nil superlayer, otherwise the behavior is
 * undefined. Nested masks (mask layers with their own masks) are
 * unsupported. */

@property(nullable, strong) CALayer *mask;

当透明度改变的时候,这个 mask 就是覆盖上去的那个阴影。该层的layer的alpha决定了多少层背景跟内容通过并显示,完全或者部分不透明的像素允许潜在的内容 通过并显示。 默认是nil,当配置一个遮罩的时候,记得设置遮罩的大小、位置。已确保跟盖图层对齐。如果你想给这个属性赋值,前提是必须没有 superLayer,如果有superLayer,这个行为则是无效的。

  • ta生来就是会触发离屏渲染的,所以要谨慎!

--

3、shadows(阴影)

在项目中,当我们想要设置View的阴影效果时,可以通过shadow*相关方法实现,如:

self.layer.shadowOffset = CGSizeMake(4, -2);  
self.layer.shadowOpacity = 0.5;  
self.layer.shadowColor = [[UIColor blackColor] colorWithAlphaComponent:0.5].CGColor;  

shadows可以给视图周边添加阴影,当给一些滑动视图加阴影时,您可能会注意到在动画不是很流畅,有卡顿。这是因为计算阴影需要Core Animation做一个离屏渲染,以View准确的形状确定清楚如何呈现其阴影。

  • 在下面的优化方案里会讲到解决方案

--

4、edge antialiasing(抗锯齿)

在目前开发过程中,抗锯齿接触的比较少。

Infer-out.png

allowsEdgeAnitialiasing:是否允许执行反锯齿边缘。

默认的值是NO(不使用抗锯齿,也有人叫反锯齿),当 Value 为YES的时候,在layer的 edgeAntialiasingMask属性layer依照这个值允许抗锯齿边缘。

  • 在真机和模拟器上的显示效果会有所偏差,但都会造成性能消耗,在平常开发中要知道怎么去避免。

--

5、group opacity(不透明)

Infer-out.png

大概的意思就是这个属性决定了Core Animation框架下,子layers从他们Superlayer继承过来的不透明度。

iOS6之前是默认NO,iOS7以后就默认是YES。文档也是说可以在模拟器上呈现,但是对性能有明显的影响。

--

6、复杂形状设置圆角

在平常开发中,我们会遇到很多对图片、按钮、view视图等设置圆角,设置圆角的方法有多种,例如:

1、设置layer层的圆角大小,也是我们最常用最简单的设置方法

self.imageView.layer.cornerRadius = 5.0f;//设置圆角度数  
self.imageView.layer.masksToBounds = YES;  

这样做对于少量的图片或控件,这个没有什么问题,但是数量比较多的时候,UITableView滑动可能不是那么流畅,屏幕的帧数下降,俗称 -- 掉帧,影响用户体验。

2、使用layer的mask遮罩和CAShapLayer

创建圆形的CAShapeLaer对象,设置为View的mask属性,这样也可以达到圆角的效果,但是前面提到过了,使用mask属性会离屏渲染,不仅这样,还增加了一个CAShapLayer对象,更加的消耗性能,不可取。

3、使用带圆形的透明图片

求一个好说话的UI

4、CoreGraphics自定义绘制圆角

如果我们重写了drawRect方法,并且使用CoreGraphics技术去绘制。就涉及到了CPU渲染,整个渲染由CPU在app内同步完成,渲染之后再交给GPU显示。(这种方式对性能的影响不是很高)

  • CoreGraphic通常是线程安全的,所以可以进行异步绘制,然后在主线程上更新。

(同第一大点的特殊的离屏渲染)


三、优化方案

官方对离屏渲染产生性能问题也进行了优化:

iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染。

iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。

--

1、圆角设置优化

在日常开发中,我们设置圆角会通过设置layer层的圆角大小来达到目的(6 - 1点),然而这种做法在滑动的时候会出现严重的掉帧现象,严重影响用户体验。

  • 方案1:使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];  
imageView.image = [UIImage imageNamed:@"myImg"];  
//开始对imageView进行画图 
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);  
//使用贝塞尔曲线画出一个圆形图 
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();  
//结束画图 
UIGraphicsEndImageContext();  
[self.view addSubview:imageView];
  • 方案2:使用CAShapeLayer和UIBezierPath设置圆角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];  
imageView.image = [UIImage imageNamed:@"myImg"];  
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继承于CALayer,可以使用CALayer的所有属性值;
  • CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果);
  • 使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形;
  • CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。

注:总的来说就是用CAShapeLayer的内存消耗少,渲染速度快

--

2、shadow优化
  • 方案1:对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。
self.imageView.layer.shadowColor = [UIColor grayColor].CGColor;  
self.imageView.layer.shadowOpacity = 1.0;  
self.imageView.layer.shadowRadius = 2.0;  
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];  
self.imageView.layer.shadowPath = path.CGPath;  
  • 方案2:有一种很简单的方式来解决该问题,既直接向Core Animation提供阴影形状,通过调用setShadowPath来提供一个CGPath给视图的Layer,(CGPath为任意你想生成的阴影的形状),如:
[myView.layer setShadowPath:[[UIBezierPath bezierPathWithRect:myView.bounds] CGPath]];
  • 再次运行起来,滑动很流畅,且没有使用离屏渲染。
3、其他的一些优化建议
  • 当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
  • 使用ShadowPath指定layer阴影效果路径
  • 使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit)
  • 设置layer的opaque值为YES,减少复杂图层合成
  • 尽量使用不包含透明(alpha)通道的图片资源
  • 尽量设置layer的大小值为整形值
  • 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
  • 很多情况下用户上传图片进行显示,可以让服务端处理圆角
  • 使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPath(CoreGraphics框架)画出来圆角图片

四、Core Animation工具检测离屏渲染

对于离屏渲染的检测,苹果为我们提供了一个测试工具Core Animation。可以在Xcode->Open Develeper Tools->Instruments中找到,如下图:

Infer-out.png

Core Animation工具用来监测Core Animation性能,提供可见的FPS值,并且提供几个选项来测量渲染性能。如下图:

Infer-out.png

Color Immediately:默认情况下Core Animation工具以每毫秒10次的频率更新图层调试颜色,如果勾选这个选项则移除10ms的延迟。对某些情况需要这样,但是有可能影响正常帧数的测试。

Color Misaligned Images:勾选此项,如果图片需要缩放则标记为黄色,如果没有像素对齐则标记为紫色。像素对齐我们已经在上面有所介绍。

Color Offscreen-Rendered Yellow:用来检测离屏渲染的,如果显示黄色,表示有离屏渲染。当然还要结合Color Hits Green and Misses Red来看,是否复用了缓存。

Color OpenGL Fast Path Blue:这个选项对那些使用OpenGL的图层才有用,像是GLKView或者 CAEAGLLayer,如果不显示蓝色则表示使用了CPU渲染,绘制在了屏幕外,显示蓝色表示正常。

Flash Updated Regions:当对图层重绘的时候回显示黄色,如果频繁发生则会影响性能。可以用增加缓存来增强性能。

  • 使用xcode自带工具,我们可以检测代码里哪里会出现离屏渲染,让我们能趁早优化,消灭影响用户体验的毒瘤 -- 掉帧。