前言
相信大部份iOS开发者在实现一些基础动画时会感叹动画API的简便及动画效果的真实性,动画是iOS的一大特色,Core Animation的存在使得我们实现一些基础动画变得十分简单,本文将会讲述iOS一些动画的基础知识,以及动画与事件链冲突的处理。
目录
-Core Animation
-Core Animation动画
-UIKit动画
-Core Animation动画与触摸响应
-定时动画
-Core Animation动画VS定时动画
-总结
Core Animation
当提起Core Animation,可能有很多人的第一反应是,Core Animation是用来做动画的,这个回答没有错,Core Animation确实是用来做动画,而且提供了非常简便的动画API,但这里有一个误区,很多人会认为Core Animation只是用来做动画的,或者大家在网上找到的大部份跟Core Animation相关的资料都只是讲述了如果使用Core Animation实现动画,这对Core Animation的认知是很片面的,Core Animation不仅仅只有动画,相反,动画只是Core Animation的一小部份。
在iOS端,你在屏幕上看到的一切都是由Core Animation来完成的,但Core Animation并不做实际的绘制渲染的工作,实际的绘制渲染工作由绘制渲染系统来完成,Core Animation是一个中间层框架。
从显示的整个流程可以得知,Core Animation为其它框架提供数据和传递数据,当然Core Animation也会对数据进行处理,像坐标,矩形计算,创建OpenGL纹理等。从上图可以看到,iOS的整个显示流程没UIKit什么事,这跟Mac OS的处理是有所不同的,iOS的显示是默认建立在Core Animation之上,显示层的处理是通过Core Animation来完成,而Mac OS并不依赖于Core Animation。由于Core Animation在iOS端的位置,所以你如何使用Core Animation这将会对性能产生极大的影响。
当你打算向可视化元素进阶的时候,Core Animation会是你进阶的第一步,也是非常关键的一步。关于Core Animation,可以看《iOS Core Animation Advanced Techniques》这本书,不知道这本书是否有更新,因为我第一次接触这本书已经是三年前了,即使没有更新,也没有关系,这并不影响书的质量,这本书可以让你全面了解到Core Animation,当然你还可以查阅苹果的官方文档,或者网上的技术文,但个人推荐看专题书,因为专题书的全面性是网上的技术文无法给予的。
Core Animation动画
Core Animation动画有两种形式,分别是显式动画和隐式动画,但不管以哪种形式实现动画,都是基于CAAnimation来实现的,所以这两种动画形式的主要差异在于,是否显式创建CAAnimation对象来实现动画,当然它们还存在其它的差异,例如,隐式动画是基于CABasicAnimation对来实现的,而显式动画可以有更多的选择,可以是CABasicAnimation,CAKeyframeAnimation,CATransition等。
对于独立CALayer而言,隐式动画是默认开启的,只需要更改可动画属性就会触发隐式动画,但对于UIView的Backing Layer而言,隐式动画是默认关闭的。当更改独立CALayer的可动画属性时,Core Animation会为我们创建默认的动画对象来实现相应的动画,但Core Animation又以什么作为标准来实现这些动画呢?隐式动画的大部份参数由事务(CATransaction)来决定,其中包括,动画时间,动画缓冲等。
虽然隐式动画的大部份参数由CATransaction来决定,但我们奇怪地发现,在代码中并没有出现CATransaction,WHY?这是由于CATransaction和NSAutoreleasePool一样,也分为隐式CATransaction和显式CATransaction,而CATransaction对隐式动画的管理方式与NSAutoreleasePool对内存的管理方式也十分相似,[CATransaction begin]方法是CATransaction的开始,而[CATransaction commit]则是CATransaction的结束,中间便是CATransaction的作用域,需要把更改可动画属性的操作放在CATransaction的作用域内,Core Animation才会创建相应的隐式动画。
上图为CATransaction的显式创建,显式创建CATransaction常常被用于关闭隐式动画(独立的CALayer对象默认开启隐式动画,需要手动关闭)和调整动画的时间。隐式CATransaction是在RunLoop的一次Loop开始时begin,Loop结束时commit,正因为有隐式CATransaction的存在,所以当你更改独立CALayer的可动画属性时,Core Animation会为此创建隐式动画。CATransaction和NSAutoreleasePool一样,可以嵌套处理。
需要注意的是,不管是显式动画还是隐式动画,动画属性的初始值要不同于目标值才会产生动画。Core Animation动画过程中,显示在屏幕上的是呈现树的对象,而不是图层树的对象。
UIKit动画
除了Core Animation的动画API,苹果还为我们准备了UIKit的动画API。UIKit实现的动画效果往往都可以满足你对动画的需求,而且实现代码非常简单,就像Core Animation的隐式动画一样,只需要在UIKit事务中修改相应可动画属性即可,所以,一般情况下UIKit动画是你优先选择的方式,但这里有一个误区,网上有很多技术文在讲述iOS动画的时候,会把UIKit动画和Core Animation动画定义成两种完全不同的处理方式,并且认为Core Animation动画的性能会更优一些,但事实并非如此,UIKit动画和Core Animation动画是相关连的,它们的关系就好像NSOperation和GCD一样,UIKit动画是建立在Core Animation动画之上的,下面我们来简单地验证一下。
一个简单的平移动画,只需要实现以上UIKit动画代码即可。
我们需要知道的是Core Animation层在UIKit动画前后的变化,通过监听animationView.layer.animationKeys来获取相关信息。
从输出的信息可以看到,UIKit动画其内部实现是Core Animation动画。UIView对象的Backing Layer的隐式动画是默认关闭的,如果想开启需要使用UIKit的事务。
UIKit事务继承了Core Animation事务(CATransaction)的所有功能,并且在此基础上做了许多扩展,像触摸响应,更多的动画对象等。作为Core Animation动画的一个抽象物,UIKit虽然提供了非常简便的API来实现动画,但UIKit动画毕竟是Core Animation动画的一个抽象物,能提供的效果还是很有限的。
Core Animation动画与触摸响应
当我们触摸屏幕时,系统就会把这个触摸事件传递给相应Application(本文不会讲述触摸事件是如何产生),Application接收到触摸事件后,执行_UIApplicationHandleEventQueue()函数(这是一个Source0的回调),把触摸事件转换成UIEvent并开始传递和处理事件。
Application处理触摸事件主要分为以下两步:
1).获取Hit-Test View。
2).寻找响应者处理事件。
响应链是由一组连接在一起的响应者组成,UIResponder是所有响应者的基类,响应者的类型不是唯一的,不同的系统事件对应着不同类型的响应者,而触摸事件的响应者主要是UIApplication对象,UIViewController对象和UIView对象,UIApplication对象是响应链的第一个节点,也是最后一个响应事件的对象。
以上两步操作由两个不同的函数完成,_UIApplicationHandleDigitizerEvent()函数用于获取Hit-Test View,[UIApplication sendEvent:]函数用于查找可以处理事件的响应者,当然这两个函数是在_UIApplicationHandleEventQueue()中被调用的,调用关系如下图。
_UIApplicationHandleDigitizerEvent()函数通过调用UIView对象的hitTest: withEvent:函数来获取Hit-Test View,hitTest: withEvent:函数简洁的伪代码如下。
一个UIWindow的hitTest: withEvent:函数返回值是当前Window的Hit-Test View,Hit-Test View是响应链中第一个被检测是否能处理触摸事件的对象。获取Hit-Test View后,调用[UIApplication sendEvent:]函数从响应链中寻找响应者处理事件,当找到响应者处理触摸事件后,本次触摸事件处理结束(当然,你也可以通过重写一些函数使事件继续传递)。
跟很多开发者交流的时候发现一个误区,很多人认为响应链是在触摸事件发生时才生成的,但事实并非如此,若响应链在触摸事件发生时才生成,那么只要不发生触摸事件就不会存在响应链,但事实上,即使不发生触摸事件,我们也可以通过nextResponder来获取上一级响应者。UIKit在UIResponder对象关系确立的同时生成响应链,响应链与视图的层级结构也是一一对应的,一般情况下,父视图是子视图的上一级响应者,当然,也有一些特殊的情况,像UIViewController根视图的上一级响应者是UIViewController而不是父视图(下图是官方文档的图)。
就像图8,图9展示的那样,一般情况下_UIApplicationHandleDigitizerEvent()函数获取Hit-Test View的过程还是比较简单的,但如果视图加上了Core Animation动画,那么获取Hit-Test View的过程就变得相对复杂了。
正常情况下,判断触摸点是否在当前图层树对象的坐标系中用的是pointInside: withEvent:函数,但也有一些特殊的情况,如果一个视图正在实现Core Animation动画且动画过程中不允许交互(默认不交互),那么该视图不再调用pointInside: withEvent:函数来判断触摸点,而是获取动画视图的呈现树对象,判断触摸点是否在该呈现树对象的坐标系中。若Hit-Test View正在实现Core Animation动画且动画过程中不允许交互,那么本次触摸事件将不处理且不再寻找响应者处理事件。
若视图正在实现Core Animation动画且动画过程中允许交互(动画过程中允许交互需要添加UIViewAnimationOptionAllowUserInteraction),那么仍是调用pointInside: withEvent:函数来判断触摸点是否在当前图层树对象的坐标系中,且正常地处理触摸事件,但当你去触摸屏幕上正在实现Core Animation动画的视图时,就会出现以下情形。
pointInside: withEvent:函数用于判断的对象是图层树中的对象,而Core Animation动画过程中,显示在屏幕上的是呈现树的对象,而不是图层树的对象,但由于图层树对象存储的是目标值,而呈现树对象存储的是瞬时值,所以,屏幕上实际的图层情况如下图。
所以当你触摸屏幕上正在实现Core Animation动画的视图时,出现了图15所展示的情形,但在实际开发当中,我们需要的是正在实现Core Animation动画的视图也可以响应触摸事件,为此,我们需要对触摸坐标的进行识别,通过获取当前触摸点,判断该点是否呈现树对象坐标系中。
定时动画
除了Core Animation动画,iOS动画还有其它实现的方式,像定时动画,手势动画等。定时动画与Core Animation动画不同,定时动画并不是基于CAAnimation来实现的,而是通过定时器的触发不断地改变图层树对象的UI属性值来实现的,每次定时触发所改变的UI属性都会被Application发送到渲染服务进程,渲染服务进程就会把最新的图层树渲染到屏幕上。通过定时器的触发不断地重复这些步骤,最终在屏幕上形成了动画。
上图显示的就是一个简单的定时动画,通过一秒60帧的频率改变图层树来实现动画效果。与Core Animation动画的触摸响应不同,定时动画是通过不断地改变图层树来实现的,屏幕上显示的是图层树对象的实时值,所以实现定时动画的视图,可以正常响应触摸事件而不需要做额外的处理。
如上图所示,实现定时动画的视图可以正常响应触摸事件。在实际开发当中,也许你从来没有实现过定时动画,但如果你想控制动画的每一帧,完全自定义动画的缓冲,那么定时动画是一个非常不错的选择,就像ScrollView一样,ScrollView的滚动就是一个定时动画,正因为如此,所以TableView才可以实时更新Cell的内容并显示出来。这种实时同步图层树的方式,既是定时动画的优点,也是定时动画的缺点,缺点在于一旦实时同步的操作时间过长就会出现屏幕掉帧的情况,最常见的例子就是TableView在滚动时出现卡屏掉帧。
Facebook的POP动画引擎就是实现为定时动画,所以不建议在列表中使用,会容易出现动画掉帧的情况。
Core Animation动画VS定时动画
不管以那种形式实现动画,动画都是一帧帧实现的,Core Animation动画与定时动画主要的不同点在于,Core Animation动画是以CAAnimation对象为基础的动画,Application通过发送图层树和CAAnimation对象到渲染服务进程,由渲染服务进程完成动画所需要的每一帧;而定时动画则是通过定时器的解发,不断地改变图层树,Application把每次改变后的图层树发送到渲染服务进程进行屏幕的重新渲染。
一般情况下,Core Animation动画会是更好的选择,不仅是因为实现Core Animation动画相对简单,而且动画的每一帧由渲染服务进程完成,Application有更多的空间来完成其它任务,提高CPU的效率;当然,你也可以使用一些物理框架来实现定时动画,自定义动画的缓冲,实现更真实的效果。更多的时候是需要根据界面的情况来选择实现动画的形式,并没有一种形式适用于任何情况,关键在于如何减少掉帧,保持界面流畅度。
总结
在iOS端,你在屏幕上看到的一切都是由Core Animation来完成的,当你打算往显示方面进阶时,你需要深入研究的是Core Animation而不是UIKit,就像Instruments里没有UIKit测试工具一样,虽然UIKit抽象了很多Core Animation的功能,但同时也带来了很多限制,有时候你更需要的是一些自定义效果。