Jerry's Blog

Less is more, Live and learning

Hello,I'm an iOS developer in China.


离屏渲染知多少?

预备知识

OpenGL中,GPU屏幕渲染有两种方式:

  • On-Screen Rendering (当前屏幕渲染):指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。
  • Off-Screen Rendering (离屏渲染):指的是在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。

当前屏幕渲染不需要额外创建新的缓存,也不需要开启新的上下文,相对于离屏渲染性能更好。但是受当前屏幕渲染的局限因素限制(只有自身上下文、屏幕缓存有限等),当前屏幕渲染有些情况下的渲染解决不了的,就使用到离屏渲染。

相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:

  • 创建新缓冲区:要想进行离屏渲染,首先要创建一个新的缓冲区。
  • 上下文切换:离屏渲染的整个过程,需要多次切换上下文环境:先从当前屏幕切换到离屏,等待离屏渲

为什么要离屏渲染?

有些效果被认为不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成。图层属性的混合体没有预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。

在iOS开发中,当设置了以下属性时,会触发离屏渲染:

  • 为图层设置遮罩(self.view.layer.mask = ?

  • 为图层设置圆角且开启遮罩属性(self.view.layer.corner = 5;self.view.layer.masksToBounds = YES

    注意:如果仅当给视图切圆角self.view.layer.corner = 5;,而没有设置遮罩属性self.view.layer.masksToBounds = YES;时,是不会触发离屏渲染的。只有设置了遮罩相关属性就会触发离屏渲染。

  • 为图层设置阴影属性时(self.view.layer.shadow

  • 为图层设置栅格化(self.view.layer.shouldRasterize = YES

  • 设置图层不透明度属性layer.allowsGroupOpacity = YES或设置layer.opacity的值小于1.0时

  • 图层设置了这些属性时:view.layer.edgeAntialiasingMask(设置边缘防锯齿遮罩),view.layer.allowsEdgeAntialiasing = YES(是否允许边缘防锯齿化为YES时,默认值:NO)

  • 使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。

    注意:这种方式触发的离屏渲染是CPU渲染,触发的CPU版本的离屏渲染,不是GPU的离屏渲染。如果我们重写了drawRect:方法并且使用任何Core Graphics Api 技术进行绘制操作时,都涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap(位图)最后再交由GPU用于显示。

  • 文本(任何种类,包括UILabel,CATextLayer,Core Text等)。

优化方案

1. 圆角优化

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

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

我们一般对UIView 、UIImageView 设置圆角时使用以下方式:

imageView.layer.cornerRadius = 5.0;
imageView.layer.masksToBounds = YES;
// 注:这种设置圆角的组合方式,就会触发离屏渲染

优化方案1:使用贝塞尔曲线UIBezierPath 和Core Graphics 组合画出一个带有圆角的UIImage,如下示例:

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 100, 100)]; 
imageView.image = [UIImage imageNamed:@"myImg"]; 
// 开启原图大小的图形上下文
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0); 
// 使用贝塞尔曲线画出一个带有圆角的路径path
[[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];

对于方案2需要解释的是:

  • CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值;
  • CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)
  • 使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形
  • CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。

总的来说就是用CAShapeLayer的内存消耗少,渲染速度快,建议使用优化方案2。

2. Shadow的优化

对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。示例如下:

// 对imageView添加阴影效果
imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;		// 阴影不透明度,1.0表示不透明
imageView.layer.shadowRadius = 2.0;			// 阴影的模糊半径
// 画出阴影路径
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;

我们还可以通过设置shouldRasterize属性值为YES来强制开启离屏渲染。其实就是光栅化(Rasterization)。

既然离屏渲染这么不好,为什么我们还要强制开启呢?当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。

当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存。所以这个功能一般不能用于UITableViewCell中,cell的复用反而降低了性能。最好用于图层较多的静态内容的图形。而且产生的位图缓存的大小是有限制的,一般是2.5个屏幕尺寸。在100ms之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。

3. 其他优化建议

圆角(主要针对ScrollView、TableView这种滑动视图上添加的视图)

  • 当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
  • 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
  • 如果能够只用cornerRadius解决,那就不要设置masksToBounds为YES,或者圆角视图数量较少且是静态页面时,也可以不用优化。

阴影、透明度等

  • 使用ShadowPath指定layer阴影效果路径
  • 使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit)
  • 设置layer的opaque值为YES,减少复杂图层合成
  • 尽量使用不包含透明(alpha)通道的图片资源
  • 尽量设置layer的大小值为整形值
  • 很多情况下用户上传图片进行显示,可以让服务端处理圆角
  • 使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPath(CoreGraphics框架)画出来圆角图片

总结

文章中首先介绍了GPU的两种渲染方式,分别是当前屏幕渲染和离屏渲染,解释了为什么离屏渲染会引起UI性能问题。并且结合了iOS日常开发中,会触发离屏渲染的几种情形。最后针对会触发离屏渲染的几个要点问题,讲解了优化方案及实践思路,希望能帮助读者解决类似问题。

参考资料

iOS的离屏渲染

离屏渲染-Alibaba

最近的文章

深入剖析Autorelease Pool (自动释放池)

前言在MRC的内存管理模式下,可以将创建的对象加入自动释放池,程序员则无需手动调用release方法来释放对象,而是当自动释放池销毁的时候会对其中的每一个对象发送release消息,从而达到自动释放的目的。下面我们一步步揭开它的神秘面纱,深度剖析autoreleasepool的实现原理。@autoreleasepool 实现原理main.m文件中的@autoreleasepool()在iOS代码main.m文件中,我们可以看到@autoreleasepool{}代码块,其中包含的这一行代码...…

iOS继续阅读
更早的文章

iOS中各种“锁”的理解及应用

前言通常在一般的iOS应用开发中会很少碰到使用“锁”的业务逻辑,但是在需要使用多线程技术,解决大多数场景写的业务逻辑时,会使用到线程锁来保证临界数据的读写安全性。当然,“锁”的概念在计算机科学及应用中也是举足轻重的,对于要写出高质量、高性能、安全可靠的代码来说,也是非常重要的。预备知识 线程调度 计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别...…

iOS继续阅读