性能调优

CPU && GPU

绘图和动画处理方式:CPU(中央处理器)和GPU(图形处理器).CPU所做的工作都在软件层面,而GPU在硬件层面。

我们可以用CPU做任何事情,但是对于图像处理,使用硬件更快,因为GPU使用图像对高度并行浮点运算做了优化。但是GPU也不是无限制使用的,一旦资源用完的性能就下降了(即使CPU没有完全占用)

我们的优化就是智能使用GPUCPU使他们都不会超过负荷,我们需要先了解这两个处理器分配工作的逻辑

动画的舞台

动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的渲染服务。在iOS6之后的版本中叫做BackBoard

当运行一段动画时,整个过程会分为4个阶段:

  • 布局 - 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。
  • 显示 - 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的- drawRect:-drawLayer:inContext:方法的调用路径。
  • 准备 - 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。
  • 提交 - 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。

这些仅仅只是发生在应用程序内内,在动画在屏幕上显示之前仍然有其他工作,一旦打包的图层和动画到了渲染服务进程,他们会被反序列化形成一个叫做渲染树的图层树,使用这个树状结构,渲染服务对动画的每一帧做出如下工作:

  • 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
  • 在屏幕上渲染可见的三角形

所以一共有六个阶段;最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。

我们
在布局和显示阶段,可以决定哪些由CPU执行,哪些交给GPU去做。

GPU相关操作

GPU为一个具体的任务做了优化:它用来采集图片和形状(三角形),运行变换,应用纹理和混合然后把它们输送到屏幕上。现代iOS设备上可编程的GPU在这些操作的执行上又很大的灵活性,但是Core Animation并没有暴露出直接的接口。除非你想绕开Core Animation并编写你自己的OpenGL着色器,从根本上解决硬件加速的问题,那么剩下的所有都还是需要在CPU的软件层面上完成。

宽泛的说,大多数CALayer的属性都是用GPU来绘制
比如如果你设置图层背景或者边框的颜色,那么这些可以通过着色的三角板实时绘制出来。如果对一个contents 属性设置一张图片,然后裁剪它 - 它就会被纹理的三角形绘制出来,而不需要软件层面做任何绘制。

但是有一些事情会降低(基于GPU)图层绘制:

  • 太多的几何结构
    太多的几何结构就需要太多的三角板来做变换,并不是GPU处理不了这么多几何结构,而是显示之前通过IPC发送到渲染服务器(图层实际上是由很多小物体组成的特别重量级的对象)时,会引起CPU的瓶颈,这就限制了一次展示的图层个数
  • 重绘

    主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以需要避免重(每一帧用相同的像素填充多次)的发生。

  • 离屏绘制
    发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着你需要避免使用这些效果,只是要明白这会带来性能的负面影响。

  • 过大的图片
    如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。

CPU相关的操作

CPU的工作对于Core Animation发生在动画开始前。所以不会影响到帧率,但是会延时动画开始时间,让界面看起来比较迟钝

延迟动画的开始时间的CPU操作:

  • 布局计算:
    视图层级过于复杂,视图修改或者呈现时计算图层帧率会消耗一部分时间

  • 视图懒加载
    iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间很有好处,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图从一个nib文件中加载,或者涉及IO的图片显示(见后续“IO相关操作”),都会比CPU正常操作慢得多。

  • Core Graphics绘制

    如果对视图实现了 -drawRect: 方法,或者 CALayerDelegate 的 -drawLayer:inContext: 方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。

  • 解压图片
    PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。为了节省内存,iOS通常直到真正绘制的时候才去解码图片,根据你加载图片的方式,第一次对图层内容赋值的时候,或者把它绘制Core Graphics中,需要解压,这样对于比较大的图片会占用时间

当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:
为了显示屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须要由CPU做这些事情。CPU涉及的工作和图层个数成正比,所以如果在你的层级关系中有太多的图层,就会导致CPU每一帧的渲染,

IO相关操作

上下文中的IO(输入/输出)指的是例如闪存或者网络接口的硬件访问。一些动画可能需要从闪存(甚至是远程URL)来加载。一个典型的例子就是两个视图控制器之间的过渡效果,这就需要从一个nib文件或者是它的内容中懒加载,或者一个旋转的图片,可能在内存中尺寸太大,需要动态滚动来加载。

IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。总的来说,这就需要使用聪敏但尴尬的技术,也就是多线程,缓存和投机加载(提前加载当前不需要的资源,但是之后可能需要用到)。

测量,而不是猜测

有很多种诡计来优化动画,但如果盲目使用的话,可能会造成更多性能上的问题,而不是修复。
我们需要正确测量性能

真机测试,而不是模拟器

  1. 真机
  2. 性能测试一定要用发布配置,而不是调试模式。因为当用发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。你也可以自己做到这些,例如在发布环境禁用NSLog语句。你只关心发布性能,那才是你需要测试的点。
  3. 最好在你支持的设备中性能最差的设备上测试.可能的话,测试不同的设备和iOS版本,因为苹果在不同的iOS版本和设备中做了一些改变,这也可能影响到一些性能。

保持一致的帧率

我们可以在程序中用CADisplayLink来测量帧率,在屏幕上显示出来,但是应用内的FPS并不能完全真实的测量出Core Animation性能,它仅仅测出应用内的帧率.但是我们知道很多动画都在应用外发生(在渲染服务器进程中处理),当然应用内的FPS可以对一些性能问题提供参考,发现问题后,我们可以通过Instruments工具集获取更多数据.

Instruments

Instruments中的Leaks工具来检测循环引用,

应该始终将程序设置成发布选项。幸运的是,配置文件默认就是发布选项,所以你不需要在分析的时候调整编译策略。

工具:
1.时间分析器(timer profile)-用来测量被方法/函数打断的CPU使用情况
2.Core Animation - 用来调试各种Core Animation性能问题。
3.OpenGL ES驱动 - 用来调试GPU性能问题,这个工具在编写Open GL代码的时候很有用,但有时也用来处理Core Animation的工作。

时间分析器

检测CPU使用情况,告诉我们程序中那个方法消耗大量CPU时间.查看CPU时间对于判断性能是不是和CPU相关,以及定位到函数都很有帮助

我们可以通过选择一些选项来定位到我们关心的方法:

  • 通过线程分离 - 这可以通过执行的线程进行分组。如果代码被多线程分离的话,那么就可以判断到底是哪个线程造成了问题。
  • 隐藏系统库 - 可以隐藏所有苹果的框架代码,来帮助我们寻找哪一段代码造成了性能瓶颈。
  • 只显示Obj-C代码 - - 隐藏除了Objective-C之外的所有代码。大多数内部的Core Animation代码都是用C或者C++函数,所以这对我们集中精力到我们代码中显式调用的方法就很有用。

Core Animation

这个工具用来检测Core Animation性能。给我们提供了周期性FPS,并考虑到了发生在程序之外的动画

提供的调试选项:

  • Color Blended Layers - 这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加)。由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。
  • ColorHitsGreenandMissesRed - 当使用shouldRasterizep属性的时候,耗时图层会被缓存,然后当做扁平化的图片呈现.当缓存再生时,这个选项用红色对栅格化图层进行了高亮。如果频繁再生就意味着栅格化可能会有负面的性能影响
  • Color Copied Images - 有时候寄宿图片的生成意味着Core Animation被强制生成一些图片,然后发送到渲染服务器,而不是简单的指向原始指针。这个选项把这些图片渲染成蓝色。复制图片对内存和CPU使用来说都是一项非常昂贵的操作,所以应该尽可能的避免
  • Color Immediately - 通常Core Animation Instruments以每毫秒10次的频率更新图层调试颜色,我们可以通过勾选这个选项来设置每帧都更新(可能影响性能)
  • Color Misaligned Images - 会高亮那些被缩放或者拉伸以及没有正确对齐到像素边界的图片
  • Color Offscreen-Rendered Yellow - 会把那些需要离屏渲染的图层高亮成黄色。这些图层很可能需要用shadowPath或者shouldRasterize来优化。
  • Color OpenGL Fast Path Blue - 会对任何直接使用OpenGL绘制的图层进行高亮。
  • Flash Updated Regions - 这个选项会对重绘的内容高亮成黄色(也就是任何在软件层面使用Core Graphics绘制的图层)。这种绘图速度很慢,频繁发生的话说明有个隐藏bug或者通过增加缓存或者使用替代方案会有提升性能的空间。

OpenGL ES驱动

OpenGL ES驱动工具可以帮你测量GPU的利用率,是一个很好的判断和GPU相关动画性能的指示器.它类似Core Animation也提供显示FPS的工具

其侧边栏选项卡中和性能相关的有:

  • Renderer Utilization - 如果这个值超过了~50%,就意味着你的动画可能对帧率有所限制,很可能因为离屏渲染或者是重绘导致的过度混合。
  • Tiler Utilization - 如果这个值超过了~50%,就意味着你的动画可能限制于几何结构方面,也就是在屏幕上有太多的图层占用了。

一个找到性能瓶颈的实例