引言
在前面的博客中我们介绍了一些Core Animation提供的关于绘制和动画的一些特性。
可以看得出Core Animation的功能和性能都非常强大,但是如果我们对背后的原理不清楚的话也会降低效率,在本篇博客中,我们就来探究一下动画运行的原理。
CPU和GPU
其实关于绘图和东湖有两种处理方式:CPU(中央处理器)和GPU(图形处理器)。在现在的iOS设备中,都有可以运行不同软件的可编程芯片,由于历史原因,我们可以说CPU所做的工作都在软件层面,而GPU所做的工作都在硬件层面。
总体来讲,我们可以使用CPU做任何事,但是对于图像的处理,通常硬件会更快,GPU在处理图像时对高度并行浮点运算进行了优化。
由于某些原因,我们想尽可能把屏幕渲染的工作交给硬件去处理。但是问题在于GPU并没有无限制处理性能,它的资源有限,一旦资源用完的话,性能就开始下降了。
所以大多数动画性能优化都是关于智能使用GPU和CPU,使它们都在合理的范围内,不超出负荷。于是我们首先需要知道Core Animation是如何在这两个处理器之间分配工作的。
动画完整流程
Core Animation 处在iOS的核心地位,其实应用内和应用间都会使用到它。
一个简单的动画可能同步显示多个APP的内容,例如当我们在多个程序间使用手势进行切换时,就会使得多个APP同时显示在屏幕上。
动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序,这个进程就是所谓的渲染服务。
当运行一段动画的时候,这个过程会被分离成四个阶段:
- 布局 - 在这个阶段开始准备视图和图层的层级关系,以及设置图层属性,位置,颜色,边框等等。
- 显示 - 这是图层的寄宿图被绘制的阶段,绘制有可能涉及-drawRect:和-drawLayer:inContext:方法的调用。
- 准备 - 这是Core Animation准备发送动画数据到渲染服务的阶段,同时也是Core Animation将要执行一些别的事务,例如解码动画过程中将要显示图片的时间点。
- 提交 - 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
但这四个阶段仅仅发生在应用程序内,动画在屏幕上显示之前仍然有很多的工作需要做,一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树的图层树,使用这个树状结构,渲染服务对动画的每一帧做如下的工作:
- 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染。
- 在屏幕上渲染可见的三角形。
所以整个过程其实有六个阶段,最后两个阶段在动画的过程中会不停地重复。而前五个阶段都在软件层面处理(CPU),只有最后一个阶段是被GPU执行的。
而且我们可以控制的其实只有前两个阶段:布局和现实。Core Animation框架在内部处理剩下的事务。
不过在布局和显示的阶段,我们仍然可以决定哪些由CUP执行,哪些由GPU执行,那么该如何判断呢?
GPU相关的操作:
GPU为一个具体的任务做了优化:它用来采集图片和形状(三角形),运行变换,应用纹理和混合然后把它们显示到屏幕上。现在的iOS设备上可编程的GPU在这些操作的执行上有很大的灵活性,但是Core Animation并没有暴漏出直接的接口。除非你想绕开Core Animation编写自己的着色器程序,从根本上解决硬件加速的问题,那么剩下的所有都还是需要再CPU的软件层面上完成。
宽泛的说大多数图层(CALayer)的属性都是用GPU来绘制的。比如如果你设置图层背景或者边框颜色,那么这些可以通过着色的三角形板实时绘制出来,如果对一个contents属性设置一张图图片,然后裁剪它,它就会被纹理的三角形绘制出来,而不需要软件层面做任何绘制。
但是是有一些事情会降低基于GPU的图层绘制,比如:
- 太多的几何结构 - 这发生在需要套多的三角板来做变换,以应对处理器的栅格化的时候。现代iOS设备的图形芯片可以处理几百万个三角板,所以在Core Animation中几何结构并不是GPU的瓶颈所在,但是由于图层在显示之前通过IPC发送渲染服务器的时候(图层实际上是由很多小物体祖册的特别重量级的对象),太多的图层就会引起CPU的瓶颈。这就限制了一次展示图层的个数。
- 重绘 - 主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以要避免重绘的发生。不过在现在的iOS设备上,GPU都会应对重绘。
- 离屏绘制 - 这发生在当不能直接在屏幕上绘制时,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着我们需要避免使用这些效果,只是要明白这会带来性能的负面影响。
- 过大的图片 - 如果视图的绘制超出了GPU支持的最大纹理尺寸,那么就必须要用CPU在图层每次显示之前对图片预处理,这同样也会降低性能。
CPU相关操作:
Core Animation的大多数关于CPU的工作都发生在动画之前,这意味着它并不会影响到帧率,但是呢它会影响动画的开始时间,会让页面看起来像是卡住了。
以下的CPU操作都会影响动画的开始时间:
- 布局的计算 - 如果你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是当我们使用自动布局的时候。
- 视图懒加载 - iOS只会在当视图控制的视图显示到屏幕上时才会加载它,这对内存的使用和程序的启动时间缩短有很大好处,但是当呈现到屏幕之前,会有许多工作都不能被及时响应,比如获取数据,或者图片显示,都会比正确情况下满许多。
- Core Graphics绘制 - 如果对视图实现了drawRect:方法,或者CALayerDelegate的drawLayer:inContext:方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束后,必须把图片数据通过IPC传到渲染服务器。再此基础上,Core Graphics绘制就会变得十分缓慢,所以在对一个性能十分挑剔的场景下这样做十分不友好。
- 解压图片 - PNG或者JPEG压缩之后的图片文件会比同质量的位图小的多,但是在图片绘制到屏幕上之前,必须把它扩展成完整的未压缩的尺寸(通常等同于图片宽*高*4个字节)。为了节省内存,iOS通常知道真正绘制的时候才去解码图片。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用UIImageView)或者把它绘制到Core Graphics中,都要对它解压,这样的话,对一个较大的图片,都会占用一定的时间。
当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:
为了让图层显示在屏幕上,Core Animation必须对渲染树中的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须要由CPU做这些事。
这里CPU涉及的工作和图层个数成正比,所以如果在你的层级关系中有太多的图层,就会导致CPU花费太多时间。
IO操作
还有一项需要提及的工作就是IO相关的操作,上下文中的IO(输入/输出)指的是例如闪存或者网络接口的硬件访问。一些动画可能需要从内存甚至是远程的URL来加载。
一个典型的例子就是两个视图控制器之间的过渡效果,这就需要从一个nib文件或者是它的内容中懒加载,或者一个旋转的图片,可能在内存中尺寸太大需要动态滚动来加载。
IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题,总的来说这就需要使用它技术比如多线程,缓存和投机加载等技术。
结语
在理解了iOS动画的底层原理后,我们能够更好地优化应用的性能,确保动画的流畅性。CPU、GPU 和 IO 各自扮演着重要的角色,协同工作以实现动画效果。然而,任何一个环节的瓶颈都可能导致动画不流畅。因此,在开发过程中,我们需要综合考虑各方面的性能因素,从而优化动画效果。通过合理分配任务、优化资源使用以及及时处理性能瓶颈,我们可以打造出更加流畅和优质的用户体验。
希望通过这篇博客,能帮助大家更好地理解iOS动画的内部工作原理,并在实际开发中运用这些知识来提升应用的动画表现。继续深入学习和探索,你会发现更多优化的可能性,让你的应用在视觉效果上更上一层楼。