iOS UI性能调优

趁着项目的短暂的空闲期,把最近关于iOS界面渲染优化的一些方法和经验记录在这里,算是一个学习和实践的总结。

基础知识

其实网上关于iOS渲染机制的文章也是数不胜数,这里推荐几篇大牛的文章给大家参考学习,相信大家在看完后会对iOS渲染机制有更加深刻的理解,本文也就不再赘述这方面的知识。同时也非常推荐iOS Core Animation advanced techniques这本书,阅读之后受益匪浅。

iOS 开发:绘制像素到屏幕
https://segmentfault.com/a/1190000000390012

深入理解 iOS Rendering Process
https://juejin.im/post/5ad3f1cc6fb9a028d9379c5f?utm_source=gold_browser_extension

性能检测

既然要进行优化,那么首先需要做的就是检测我们项目中当前的性能状况,苹果的Xcode自带的Instrument就可以很好的完成这项工作,使用方法也很简单。这里以最新版Xcode10为例子,点击Xcode->Open Developer Tool ->Instruments,启动Instruments, 如下图,点击Core Animation模块
instrument-coreanimation
会出现如下面板,在All Processes这里可以选择你想要测试的项目
这里我选择了公司的项目,然后点击左上角的红圈,instrument就运行了,你会发现你手机上安装的该项目也会随之运行起来。
instrument-debug
然后就会出现上图,显示的是项目运行的FPS,以及GPU的利用率。当画面静止的时候,FPS为0。一般来说,FPS应当要大于45才不会显得卡顿。

影响FPS的因素是多样的, 这里我们只考虑UI方面的因素,我们可以在Xcode10工具栏的Debug -> View Debugging -> Rendering进行查看
rendering
可以看到这里有很多Debug选项, 我们来看几个比较重要的

Color Blended Layers
图层混色,就是多个视图的位置有重叠,视图本身又有透明度。重叠区域的每一个像素,GPU 需要算出一种新的颜色(混色)。混合计算的公式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

贴一个苹果文档中的解释

>```objective-c
>The blend mode constants introduced in OS X v10.5 represent the Porter-Duff blend modes. The symbols in the equations for these blend modes are:
>
> * R is the premultiplied result
>
> * S is the source color, and includes alpha
>
> * D is the destination color, and includes alpha
>
> * Ra, Sa, and Da are the alpha components of R, S, and D
>

R是结果色,S是包含透明度的源色,D是包含透明度的目标色,Sa是原色的透明度

如下图,绿色代表没有发生图层混色,红色代表发生了图层混色。图中的label2设置了alpha为0.5,因此触发了Color Blended; 我们通过给View设置一个不透明的背景色,来避免图层混色的发生。

blending

关于Color Blended Layers有几个注意点

  • 如果label中的内容包含中文,label实际渲染区域要大于label的size,最外层会多一个subLayer,所以即使设置背景色不透明,仍然会发生混色。因此,还需要添加masksToBounds = YES
  • UIImageView不仅需要自身容器不透明,并且imageView包含的内容图片也必须是不透明的
  • 如果使用的是CALayer,要把opaque属性设置成YES(默认是NO)。如果是用的UIView, opaque属性默认是YES。

Color Hits Green and Misses Red

这个选项是用来检测图像是否有光栅化(Rasterize)的,这个概念听起来是一头雾水,百度之; 光栅化即将矢量图形转化为位图(栅格图像) , Calayer有一个属性shouldRasterize,就是用来决定是否开启光栅化的。

放一个苹果的文档

1
When true, the layer is rendered as a bitmap in its local coordinate  space ("rasterized"), then the bitmap is composited into the destination (with the minificationFilter and magnificationFilter  properties of the layer applied if the bitmap needs scaling). Rasterization occurs after the layer's filters and shadow effects are applied, but before the opacity modulation. As an implementation  detail the rendering engine may attempt to cache and reuse the bitmap from one frame to the next. (Whether it does or not will have no affect on the rendered output.) When false the layer is composited directly into the destination whenever possible (however, certain features of the compositing model may force rasterization, e.g. adding filters). Defaults to NO.

当这个属性被设置为YES时,即开启了光栅化。它会将一个layer预先渲染成bitmap再加入到缓存中,layer的阴影等效果也会被保存到bitmap中,光栅化后会将图层绘制到一个屏幕外的图像,然后这个图像将会被缓存起来并绘制到实际图层的 contents 和子图层,对于有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧来更加高效。但是光栅化原始图像需要时间,而且会消耗额外的内存。

当我们开启光栅化后,需要注意几点问题:

  1. 如果我们更新已光栅化的layer,会造成大量的离屏渲染。

    CALayer的光栅化选项的开启与否需要我们仔细衡量使用场景。它主要适用于设置阴影耗费资源比较多的静态内容,而对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费。例如TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可栅格化。则会造成大量的offscreen渲染,降低图形性能。

  2. 不要过度使用,系统限制了缓存的大小为2.5X Screen Size. 如果过度使用,超出缓存之后,同样会造成大量的offscreen渲染。

  1. 被光栅化的图片如果超过100ms没有被使用,则会被移除。

    因此我们应该只对连续不断使用的图片进行缓存。对于不常使用的图片缓存是没有意义,且耗费资源的。

Color Offscreen-Rendered Yellow 离屏渲染

离屏渲染应该是经常听到的一次词了,简单来说就是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作,由于过程中需要切换context,性能消耗比较大。

CoreGraphics的上下文绘制,drawRect绘制,layer圆角/边框/阴影/抗锯齿/光栅化等都可能导致离屏渲染 (Core Graphics 的绘制API出发的离屏渲染不是那种GPU的离屏渲染,使用Core Graphics 绘制 API 是在 CPU 上执行,触发的是 CPU 的离屏渲染。)

在Debug选项中选中Color Offscreen-Rendered Yellow,就可以检测视图的离屏渲染情况。如下图所示,呈现黄色的部分为发生了离屏渲染。

offscreen

offscreen经过真机测试,发现以下情况

  • 在iOS9系统上, 对UIImageView/UILabel/Button,使用cornerRadius + masksToBounds/ClipToBounds会发生离屏渲染, 但是对于UIButton,只需要设置cornerRadius就可以实现圆角效果,不会发生离屏渲染。
  • 在iOS11上, 对UIImageView/UILabel,使用cornerRadius + masksToBounds/ClipToBounds并不会触发离屏渲染,但是在设置了shadow之后会触发离屏渲染。

在查找了网上的一些资料后,了解到苹果在iOS9之后做了优化,减少了不必要的离屏渲染。

针设置圆角/阴影导致的离屏渲染的处理方式

  • 如果方便的情况下,可以让设计师直接提供裁切好圆角的图片。
  • 对于UIView,只设置CornerRadius,无需设置ClipToBounds就可以实现圆角效果,不会触发离屏渲染。
  • 对于UILabel,只设置CornerRadius,无需设置ClipToBounds就可以实现圆角效果,不会触发离屏渲染;如果label有背景色,在iOS10以上系统,可以使用CornerRadius + ClipToBounds组合,10以下的系统,可以设置label.layer.backgroundColor来代替label.backgroundColor
  • 对于UIImageView 如果只需要支持iOS10及更新版本的机型,那么大胆的使用cornerRadius + masksToBounds,不会触发离屏渲染; 10以下的机型,可以通过给UIImage添加Category,利用UIBezierPath来实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    - (UIImage*)imageAddCornerWithRadius:(CGFloat)radius andSize:(CGSize)size{
    CGRect rect = CGRectMake(0, 0, size.width, size.height);
    UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
    CGContextAddPath(ctx,path.CGPath);
    CGContextClip(ctx);
    [self drawInRect:rect];
    CGContextDrawPath(ctx, kCGPathFillStroke);
    UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
    }

  • 对于UIButton,如果只需要实现文字 + 圆角效果,那么用ConerRadius就可以了;如果要实现有图片的Button的圆角效果, 可以先参照上述方法先对图片进行处理。

  • 对于简单阴影,可以使用CGContexRef/UIBezierPath绘制阴影路径并设置给ShadowPath来代替shadowOffset等属性设置阴影,下面是关于shadowPath的官方解释

    shadowPath

    1
    2
    3
    4
    5
    imageView.layer.shadowColor = [UIColor grayColor].CGColor;
    imageView.layer.shadowOpacity = 1.0;
    imageView.layer.shadowRadius = 2.0;
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
    imageView.layer.shadowPath = path.CGPath;