Instruments 之 图像绘制性能优化(三)

绘图和动画有两种处理方式,CPU和GPU(图形处理器)。CPU理论上可以做任何事情,但对于图像处理,通常硬件会更快,因为GPU使用图像对高度并行的浮点运算做了优化。通常会把屏幕渲染工作交给GPU来处理,但当GPU资源用完的话,性能就逐渐下降了。
大多数动画性能优化都是关于对GPU和CPU使用的优化,不超出负荷。

当运行一段动画时,分为四个阶段

  • 1.布局 - 确定视图层级关系,以及设置图层属性(位置,背景色,边框等)。
  • 2.显示 - 绘制图层寄宿图片,绘制有可能涉及你的- drawRect:-drawLayer:inContext:方法的调用路径。
  • 3.准备 - 准备发送动画数据到渲染服务。
  • 4.提交 - Core Animation 打包所有图层和动画属性,然后发送到渲染服务显示。
    在动画在屏幕上显示之前仍然有更多的工作。一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树的图层树。使用这个树状结构,渲染服务对动画的每一帧做出如下工作:
  • 5.对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
  • 6.在屏幕上渲染可见的三角形

注:最后5,6两个阶段在动画过程中不断地重复执行,其中前五个阶段在软件层面通过CPU处理,最后一个渲染通过GPU来执行。开发者可以控制的只有前两个阶段:布局和显示。开发者可以再这两个阶段决定哪些由CPU执行,哪些由GPU处理。

如何对GPU操作进行优化

GPU 的优化体现在采集图片和形状(三角形),运行变换,应用纹理及混合然后输送到屏幕的过程。如果想要在这些操作上有更多的灵活性,可以让开Core Animation自己实现OpenGL着色器,从根本上解决硬件加速问题。我们这里只考虑如何从软件层面优化

1.一般来说CALayer 的属性都是用GPU来绘制的。比如设置图层背景,边框颜,这些可以通过着色三角板实时绘制出来。如果对一个contents属性设置一张图片,然后剪裁,就是通过GPU直接绘制。

避免降低GPU效率的图层绘制
  • 避免太多的几何结构,在Core Animation中几何结构并不是GPU的瓶颈,但太多的图层会引起CPU的瓶颈,限制一次展示的图层个数
  • 避免每一帧用相同的像素填充多次,即重绘,主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以要避免重绘。当然在硬件的提升下,适量的重绘不影响性能。
  • 避免离屏绘制,离屏绘制一般发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文的时候。离屏绘制发生在基于CPU或者GPU渲染或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着你需要避免使用这些效果,只是需要知道这会带来负面影响。
  • 避免过大的图片,如果视图绘制超过GPU支持的20482048或者40964096尺寸的纹理,就必须要CPU在图层没实现是之前对图片进行预处理,同样也会降低性能。

如何对CPU操作进行优化

一般CPU处理都会在动画开始之前,不会影响到帧率,但是会延迟动画开始的时间,使界面看起来迟缓。
影响动画开始时间的CPU操作有:

  • 布局计算,当视图层级过于复杂,当视图呈现或者修改时,计算图层帧率就会消耗一部分时间。特别是使用AutoLayout尤为明显,AutoLayout比以前的AutoResizing消耗更多的CPU。
  • 视图的懒加载,一个试图将要显示到屏幕时才会加载它。者对于内存使用和缩短程序启动时间很有帮助,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图从一个nib文件中加载,或者涉及IO的图片显示(见后续“IO相关操作”),都会比CPU正常操作慢得多。
  • Core Graphics绘制 - 如果对视图实现了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
  • 解压图片 - PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用UIImageView
    )或者把它绘制到Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。
  • I/O相关的优化
    如果动画需要I/O操作来加载,
    IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。总的来说,这就需要使用聪敏但尴尬的技术,也就是多线程,缓存和投机加载(提前加载当前不需要的资源,但是之后可能需要用到)。

如何测量是否有必要优化

  • 真机提供参数的真实性。不同硬件上测试,关注用户的设备以及系统。
  • 保持一定的帧率,为了做到动画的平滑,需要以60FPS(帧每秒)的速度运行,同步屏幕刷新频率。如果不保持60FPS的速率,可能随机丢帧,影响体验。
Core Animation Debug Options & 优化
Core Animation2

Color Blended Layers (图层混合)
界面都是会出现多个UI控件叠加的情况,如果有透明或者半透明的控件,那么GPU会去计算这些这些layer最终的显示的颜色。基于渲染程度对屏幕中混合区域进行绿到红的高亮显示,越红表示性能越差,会对帧率等指标造成较大影响。

优化方法:
如果出现图层混合了,如何优化呢?
(1)设置opaque 属性为true。(2)调整布局(3)给View设置一个不透明的颜色,没有特殊需要设置白色即可。
label.backgroundColor = [UIColor whiteColor];
label.layer.masksToBounds = YES;

如果label的内容是中文,label实际渲染区域要大于label的size,最外层多了一个sublayer,如果不设置masksToBounds,边缘外层会出现图层混合的红色。
单独使用layer.masksToBounds = YES是不会发生离屏渲染。
UIImageView控件比较特殊,不仅需要自身这个容器是不透明的,并且imageView包含的内容图片也必须是不透明的,如果你自己的图片出现了图层混合红色,如果确认代码没问题,就是图片自身的问题

Color Hits Green and Misses Red(光栅化)
当UIView.layer.shouldRasterize = YES 时,耗时的图片绘制会被缓存,并当做一个简单的扁平图片来呈现。
适用情况:一般在图像内容不变的情况下才使用光栅化,例如设置阴影耗费资源比较多的静态内容,如果使用光栅化对性能的提升有一定帮助。
不适用的情况:如果内容会经常变动,这个时候不要开启,否则会造成性能的浪费。例如我们在使用tableViewCell中,一般不要用光栅化,因为tableViewCell的绘制非常频繁,内容在不断的变化,如果使用了光栅化,会造成大量的离屏渲染降低性能。
(1)系统给光栅化缓存分配了一个固定的大小,因此不能过度使用,如果超出了缓存也会造成离屏渲染。(2)缓存的时间为100ms,因此如果在100ms内没有使用缓存的对象,则会从缓存中清除。

Color Copied Images(图片颜色格式)
如果使用Color Copied Images去调试发现是蓝色,可以去找UI重新做一张。苹果的GPU只解析32bit的颜色格式,对于GPU不支持的色彩格式的图片只能由CPU来处理,这样的图片为蓝色,蓝色越多性能越差。因为如果在滚动加载大量图片时,由CPU来处理图片,可能会阻塞主线程。
知识扩展:32bit指的是图片颜色深度,用“位”来表示,用来表示显示颜色数量,例如一个图片支持256种颜色,那么就需要256个不同的值来表示不同的颜色,也就是从0到255,二进制表示就是从00000000到11111111,一共需要8位二进制数,所以颜色深度是8。通常32bit色彩中使用三个8bit分别表示R红G绿B蓝,还有一个8bit常用来表示透明度(Alpha)。
Color Immediately(颜色刷新频率)
通常Core Animation Instruments以每毫秒10次的频率更新图层调试颜色,如果某些效果觉得不够用,这个选项可以设置每帧都更新(不建议使用,可能会影响到渲染性能)
Color Misaligned Images(图片大小)
这个选项检查了image size和imageView size不匹配,图片是否被缩放,以及像素是否对齐。被放缩的图片会被标记为黄色,像素不对齐则会标注为紫色。黄色、紫色越多,性能越差。

Color Offscreen-Rendered Yellow(离屏渲染)
将离屏渲染标为黄色,黄色越多性能越差,可以用shadowPath 或者 shouldRasterize 来优化。
触发离屏渲染Offscreen rendering的行为:
(1)drawRect:方法
(2)layer.shadow
(3)layer.allowsGroupOpacity or layer.allowsEdgeAntialiasing
(4)layer.shouldRasterize
(5)layer.mask
(6)layer.masksToBounds && layer.cornerRadius
这里有需要注意的是第三条layer.shouldRasterize ,其实就是我们本文讲的第三个选项光栅化,光栅化会触发离屏渲染,因此光栅化慎用。
第六条设置圆角会触发离屏渲染,如果在某个页面大量使用了圆角,会非常消耗性能造成FPS急剧下降,设置圆角触发离屏渲染要同时满足下面两个条件:
layer.masksToBounds = YES;
layer.cornerRadius = 5;

为了尽可能避免触发离屏渲染,我们可以换其他手段来实现必要的功能:

/**
 * Method : CAShapeLayer设置圆角
 * rectCorner  : UIRectCorner
 * 需要导入<AVFoundation/AVFoundation.h>
 */
- (void)shapeLayerCornerRadiusWithView:(UIView *)view withRectCorner:(UIRectCorner)rectCorner
{
    //使用UIBezierPath 和 CAShapeLayer 设置圆角 内存占用最小且渲染快速

    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:view.bounds.size];
    
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
    //设置大小
    maskLayer.frame = view.bounds;
    //设置图形样子
    maskLayer.path = maskPath.CGPath;
    view.layer.mask = maskLayer;
}


/**
 * Method : shadowPath设置圆角
 * Return : return
 */
- (void)shadowPathOfView:(UIView *)view{
    
    CGMutablePathRef squarePath = CGPathCreateMutable();
    CGPathAddRect(squarePath, NULL, view.bounds);
    view.layer.shadowPath = squarePath;
    CGPathRelease(squarePath);
}

Color Compositing Fast-Path Blue (快速路径)
将直接使用OpenGL绘制的图层显示为蓝色。蓝色越多,性能越好。如果使用CLKView或者CAEAGLLayer,但不显示蓝色则说明你正在强制CPU渲染额外的纹理,而不是绘制到屏幕。
Flash Updated Regions (重绘区域)
将重新绘制的内容显示为黄色,尽量减少不需要的重新绘制。这种绘图的速度很慢。如果频繁发生这种情况的话,这意味着有一个隐藏的bug或者说通过增加缓存或者使用替代方案会有提升性能的空间。

OpenGL ES Analysis 测量GPU的利用率

OpenGL ES Analysis

OpenGL ES Analysis

Renderer Utilization - 如果这个值超过了~50%,就意味着你的动画可能对帧率有所限制,很可能因为离屏渲染或者是重绘导致的过度混合。

Tiler Utilization - 如果这个值超过了~50%,就意味着你的动画可能限制于几何结构方面,也就是在屏幕上有太多的图层占用了。

推荐阅读更多精彩内容