Core Animation & Facebook's POP


前言

相信很多人对实现 iOS 中的动画效果都特别头疼,往往懒得动手,功能实现不就得了,何必要那么花哨、装13的东西。但是看到别人的炫酷动效,心中又瘙痒不已,便下定决心学习,于是开始翻看 Core Animation、UIView动画(其实是对Core Animation的一种封装)相关资料。不小心看到一群大神正在热烈讨论,钻一进去一看,原来是 POP (潜意识:Facebook出品必属精品),这还学什么Core Animation,果断pod一个来玩玩,于是你就左手CA,右手 POP 开森地把玩起来了。

此时,你可能已经学会了CA的基本使用方法,也对UIView动画的便捷感到惊喜,但是不满足的你,显然有更高的追求,POP 以其灵活的用法,丰富的动效,完整的API文档,深得很多程序员的喜爱。作为一个有逼格的程序员,这么流行的框架,必然是值得深入学习的,但是你是否考虑过这样的第三方动画框架是否存在什么不足。因此,作为一个有追求的程序员,有必要来稍微深入地探讨一下 Core Animation 和 POP 不同点。

Core Animation 工作机制

首先我们需要了解CA是如何工作的。每当我们创建并添加动画到 layer 时,QuartzCore 框架就会把动画的参数打包好,然后通过 IPC (处理器)发送给名为 backboardd 的后台处理程序。你的应用也会发送当前展示在屏幕上的每一个 layer 的信息。

backboardd 会处理 layer 的结构体系然后通过 OpenGL 绘制出来。它还会处理你已经添加过的动画(也可以是视图,因为视图本质是包裹着 layer的)。你一定要理解的是,backboardd 使得动画的每一帧都可以在你的应用中完全独立。这里唯一的回调是动画的开始和结束(详见CAAnimationDelegate 协议)。你的应用完全不会参与动画的绘制,这些绘制完全独立于你的应用进程(除非你明确地在你的应用中通过动画通用属性要求绘制动画帧)。这意味着你可以继续在主线程做其他事情,并且不会影响到 CAAnimation 的性能。如果你阻塞了你的主线程,或者你在调试器中暂停了你的程序,你的动画还是会继续执行。

但是你可能会有这样的疑问:每个 CALayer 不是还有一个 presentationLayer 属性吗?

presentationLayer的官方解释:

“While an animation is in progress, you can retrieve this object and use it to get the current values for those animations.”

当CAAnimation发生时,你在屏幕上看到的实际上是 presentation layer 的改变。如果你访问 presentation layer,QuartzCore 将会计算现有的帧状态,并且使用这个帧状态去构建 presentation layer 对象。因为动画状态在动画执行期间一直处于改变,因此你将会获得近似值。

POP 工作机制

现在有很多优秀的第三方动画库,POP 因为其使用灵活、功能强大、文档齐全,所以备受好评,先看一下官方介绍:

POP是一个在iOS与OS X上通用的极具扩展性的动画引擎 它在基本的静态动画的基础上增加的弹簧动画与衰减动画
使之能创造出更真实更具物理性的交互动画 POP的API可以快速的与现有的ObjC代码集成并可以作用于任意对象的任意属性
POP是个相当成熟且久经考验的框架 Facebook出品的令人惊叹的Paper应用中的所有动画和效果即出自POP

更为详细的介绍和使用请查看官方文档以及里脊串的 POP介绍与使用实践(快速上手动画)

POP 本质上是基于定时器的动画库,使用每秒 60 频率的定时器,即时钟频率为 1/60 秒(为了匹配 iOS 显示屏帧率),使得动画刷新绘制频率与屏幕刷新频率一致。很多这类动画库都使用 CADisplayLink 做为一个回调源。

一旦定时器刷新,动画库计算动画的进程,这意味着动画库会计算那些活动的东西的状态(通常是layer 属性,如 bound,opactiy,transform 等)。然后动画库提供最新计算的值给有动画的 layer (或者其他对象)。最主要的区别是,layer 的状态将会在这种情况下改变。

由于 layer 的一些参数已经被改变,你的应用必须通过 IPC 通知 backboardd 处理这些变化。当 backboardd 接收到变化通知(同时接收到的还有应用中的 layer 树),它将在屏幕上重绘一切东西。这意味着,你应用中做的每一个动画帧都会传送数据到 backboardd (即通知 backboardd ),因为 backboardd 完全不知道 layer 发生了什么事情。综上,你的应用就是在这种情况下运行动画的。

Core Animation 和 POP 运行动画对比

由于 POP 是基于定时器定时刷新添加动画的原理,那么如果将动画库运行在主线程上,会由于线程阻塞的问题导致动画效果出现卡顿、不流畅的情况。更为关键的是,你不能将动画效果放在子线程,因为你不能将对 view 和 layer 的操作放到主线程之外。

为了验证上述的观点,我做了一个实验,首先用CA动画制作一个可以旋转的 view:

1
2
3
4
5
6
7
8
UIView *viewCA = [[UIView alloc]initWithFrame:CGRectMake(50,50, 100, 100)];
viewCA.backgroundColor = [UIColor blueColor];
[self.view addSubview:viewCA];
CABasicAnimation *caAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
caAnimation.toValue = @(M_PI);
caAnimation.duration = 2.0;
caAnimation.repeatCount = 500;
[viewCA.layer addAnimation:caAnimation forKey:@"anim"];

再创建一个利用 POP 动画库制作的可旋转 view:

1
2
3
4
5
6
7
8
9
UIView *viewPOP = [[UIView alloc]initWithFrame:
CGRectMake(CGRectGetWidth(self.view.bounds) - 100 - 50, 50, 100, 100)];
viewPOP.backgroundColor = [UIColor yellowColor];
[self.view addSubview:viewPOP];
POPBasicAnimation *popAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerRotation];
popAnimation.toValue = @(M_PI);
popAnimation.duration = 2.0;
popAnimation.repeatCount = 500;
[viewPOP.layer pop_addAnimation:popAnimation forKey:@"rotation"];

在没有线程阻塞的情况下,对比两个动画库的运行效果如下:

这里写图片描述

可以看出来虽然在没有线程阻塞,但是 POP 的动画在结束时有一个明显的停止动作,是因为 POP 的动画效果不好吗?

答案是 timingFunction

CoreAnimation 和 POPBasicAnimation提供同样的四种 timingFunction

kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault

重点说一下:kCAMediaTimingFunctionDefault(引自:iOS-Core-Animation-Advanced-Techniques(五)

它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut的区别很难察觉,可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作为默认效果),虽然它的名字说是默认的,但还是要记住当创建显式的CAAnimation它并不是默认选项(换句话说,默认的图层行为动画用kCAMediaTimingFunctionDefault作为它们的计时方法)。

如果不设置 timingFunction 属性,那么在使用 CA 的情况下, timingFunctionkCAMediaTimingFunctionLinear 的,而 POP 却是kCAMediaTimingFunctionEaseOut ,因此我们只要添加这么一行代码:

1
popAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];

现在再看效果:

这里写图片描述

可以看出来,在主线程没有阻塞的情况下,两种动画库的表现并无差异(POP 就是🐂)

现在我们来制造一点难度,人工利用线程的 sleep 增加一个主线程阻塞:

1
2
3
4
5
6
- (void)repeatedlyBlockMainThread
{
NSLog(@"blocking main thread!");
[NSThread sleepForTimeInterval:0.25];
[self performSelector:@selector(repeatedlyBlockMainThread) withObject:nil afterDelay:1];
}

然后再 viewDidLoad 里面调用 :

1
[self performSelector:@selector(repeatedlyBlockMainThread) withObject:nil afterDelay:1];

现在再来看一下两者的动画效果:

这里写图片描述

很明显,我们可以看出来,由于添加了主线程阻塞,利用 POP 制作的动画视图,在每隔 1s 都会卡顿一下,而 CA 的视图却完全不受主线程阻塞的影响。

总结

通过这次简单的对比,我们从工作机制上了解了 CA 和 POP 两个动画库的基本原理,并用简单的动画效果对比,重现了在主线程阻塞的情况下两者的差异,很显然, POP 受主线程阻塞的影响很大,在使用过程中,应避免在有可能发生主线程阻塞的情况下使用 POP ,避免制作卡顿的动画效果,产生不好的用户体验。文中提出了 POP 的这种缺点,但是 POP 毕竟是久经考验的动画技术,本人也正在学习中,有错误的地方吝请指正。

对比系列,是个人比较喜欢的一种学习方式,通过对比,找出不同技术的优缺点,可以更合理地使用这些武器,俗话说:好钢用在刀刃上,大抵如此。