一、引出问题
在开始分析原理之前,我们先来看一个问题:
我们都知道 UIView
与 CALayer
之间的关系,通俗的来说,UIView
内部封装了一个 CALayer
, 其中 CALayer
负责展示UI,而 UIView
负责处理交互事件。
其中 UIView
的所有UI信息都会对应到 CALayer
上,即改变 UIView
的位置信息与改变 CALayer
的位置信息效果是一样的。
由上述结论,如果我们只是展示UI而无需交互,那么可以使用 CALayer
来替代 UIView
,从而避免多余的内存占用。
既然如此,下面代码说展示的三个视图表现应当是一致的:
@interface ViewController ()
@property (nonatomic, strong) UIView *redView;
@property (nonatomic, strong) UIView *greenView;
@property (nonatomic, strong) CALayer *yellowLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.redView removeFromSuperview];
self.redView = nil;
[self.greenView removeFromSuperview];
self.greenView = nil;
[self.yellowLayer removeFromSuperlayer];
self.yellowLayer = nil;
self.redView = [[UIView alloc] initWithFrame:CGRectMake(0, 100, 50, 50)];
self.redView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.redView];
self.greenView = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 50, 50)];
self.greenView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.greenView];
self.yellowLayer = [[CALayer alloc] init];
self.yellowLayer.frame = CGRectMake(0, 300, 50, 50);
self.yellowLayer.backgroundColor = [UIColor yellowColor].CGColor;
[self.view.layer addSublayer:self.yellowLayer];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.redView.frame = CGRectMake(200, 100, 50, 50);
self.greenView.layer.frame = CGRectMake(200, 200, 50, 50);
self.yellowLayer.frame = CGRectMake(200, 300, 50, 50);
});
}
@end
但真实情况确实如下图所示:
对 UIView
和 UIView
的 layer 设置 frame,视图会直接显示在对应位置,但对 CALayer
设置 frame,则会有一个移动的动画,这是怎么回事呢?
如果百度一下,所有答案都会是 隐式动画
,但隐式动画是个什么,为 CALayer
设置 frame 为什么会有隐式动画,为什么为 UIView
的 layer 设置 frame 却没有触发隐式动画呢?
这些问题将会在我们分析动画原理的过程中得到解答。
二、CALayer展示原理
既然上述代码表现出不同的UI,那么我们就从展示UI的 CALayer
出发来找寻答案。
CALayer
有许多属性,其中有两个只读属性较为特殊:
- (nullable instancetype)presentationLayer;
- (instancetype)modelLayer;
这两个属性分别对应的是 CALayer
的展示层和模型层,这两个属性的用处及关系如下:
- CALayer的modelLayer默认为CALayer本身,modelLayer的作用为存储CALayer真实的UI信息,而不显示在屏幕上,所以称modelLayer为模型层;
- presentationLayer的作用为在屏幕每次刷新时(iPhone频率为60次/秒),从modelLayer中获取最新的UI信息,然后进行渲染展示在屏幕上,所以称presentationLayer为显示层;
- presentationLayer在屏幕刷新时,若CALayer层有CAAnimation存在,那么presentationLayer会忽略modelLayer中存储的UI信息,直接从CAAnimation中计算出对应UI信息,然后进行渲染展示。
由以上 CALayer
展示层和模型层之间的关系,我们可以解释一些在使用 CAAnimation
过程中遇到的一些问题:
1. CAAnimation 动画执行完毕后为什么视图会变回动画前状态?
原因在于CAAnimation的 removedOnCompletion
属性默认为 YES
,当动画播放完毕后,animation被移除,在下一次刷新时,presentationLayer会从modelLayer请求UI信息,从而会跳到动画执行前状态。
这种情况网上也提供了解决方案,就是设置removedOnCompletion为NO,这就保证了动画结束后presentationLayer无法向modelLayer请求UI信息,避免跳回动画执行前状态(这种方案需要搭配fillMode使用,fillModel的内容请自行百度)。
2. CAAnimation 动画执行中,UIView事件响应范围与UI显示范围不一致
这个问题很好解释,虽然在播放动画,但CALayer的响应范围信息保存在modelLayer中,从而导致响应范围与UI显示范围不一致。
三、动画从何而来
至此,我们知道了CALayer在展示时会受到animation的影响,那在单使用CALayer显示UI并改变UI信息时,是如何产生animation的呢?
不论animation如何产生,最终都会生成一个 CAAnimation
对象,那我们就通过hook CAAnimation的初始化方法,从而获取动画的生成流程。
+ (void)load
{
[self swizzleClassMethod:@selector(animation) withMethod:@selector(sg_animation)];
[self swizzleInstanceMethod:@selector(init) withMethod:@selector(sg_init)];
}
+ (instancetype)sg_animation
{
NSLog(@"---call %@", [NSThread callStackSymbols]);
return [self sg_animation];
}
获得如下打印结果:
0 ViewTest 0x0000000101858b0f +[CAAnimation(Test) sg_animation] + 47
1 QuartzCore 0x00007fff2b148422 CALayerCreateImplicitAnimation + 300
2 QuartzCore 0x00007fff2b1aa0a4 +[CATransaction(CATransactionInternal) _implicitAnimationForLayer:keyPath:] + 80
3 QuartzCore 0x00007fff2b134312 -[CALayer actionForKey:] + 558
4 QuartzCore 0x00007fff2b13aa05 _ZN2CA5Layer12begin_changeEPNS_11TransactionEjP11objc_objectRS4_ + 185
5 QuartzCore 0x00007fff2b144043 _ZN2CA5Layer12set_positionERKNS_4Vec2IdEEb + 277
6 QuartzCore 0x00007fff2b132460 -[CALayer setPosition:] + 49
7 QuartzCore 0x00007fff2b132b05 -[CALayer setFrame:] + 563
8 ViewTest 0x00000001018596a2 __29-[ViewController viewDidLoad]_block_invoke + 706
9 libdispatch.dylib 0x0000000101b7fd48 _dispatch_client_callout + 8
10 libdispatch.dylib 0x0000000101b826ba _dispatch_continuation_pop + 552
11 libdispatch.dylib 0x0000000101b95a0f _dispatch_source_invoke + 2205
12 libdispatch.dylib 0x0000000101b8dc1d _dispatch_main_queue_callback_4CF + 1043
13 CoreFoundation 0x00007fff23bb1df9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
14 CoreFoundation 0x00007fff23baca59 __CFRunLoopRun + 2329
15 CoreFoundation 0x00007fff23babe16 CFRunLoopRunSpecific + 438
16 GraphicsServices 0x00007fff38438bb0 GSEventRunModal + 65
17 UIKitCore 0x00007fff4784fb48 UIApplicationMain + 1621
18 ViewTest 0x0000000101859ac4 main + 116
19 libdyld.dylib 0x00007fff51a1dc25 start + 1
20 ??? 0x0000000000000001 0x0 + 1
从打印结果可以看出,创建animation的入口是在 CALayer
的 actionForKey:
方法处,我们可以看一下这个方法执行了什么,官方文档如下:
/* Returns the action object associated with the event named by the
* string 'event'. The default implementation searches for an action
* object in the following places:
*
* 1. if defined, call the delegate method -actionForLayer:forKey:
* 2. look in the layer's `actions' dictionary
* 3. look in any `actions' dictionaries in the `style' hierarchy
* 4. call +defaultActionForKey: on the layer's class
*
* If any of these steps results in a non-nil action object, the
* following steps are ignored. If the final result is an instance of
* NSNull, it is converted to `nil'. */
- (nullable id<CAAction>)actionForKey:(NSString *)event;
该方法在CALayer的属性发生变化时会被调用,然后会根据对应的event来寻找一个遵循 CAAction
协议的对象,寻找流程如下:
- 如果CALayer的delegate存在并实现了
actionForLayer:forKey:
方法,那么调用该方法寻找; - 从CALayer的actions字典中根据event为key值寻找;
- 从CALyaer的style层级中的actions字典中根据event为key值寻找;
- 调用类方法
+defaultActionForKey:
方法寻找。
在以上步骤中,只要有一步找到action,那么就停止,如果最终获取到的是Null对象,那么会被自动转为nil。
从打印结果还可以看出,CALayer
基类在以上步骤中未找到action后,会调用 [CATransaction(CATransactionInternal) _implicitAnimationForLayer:keyPath:]
生成对应的action,我们可以打印一下该action,看看具体是什么。
(lldb) po action
<CABasicAnimation:0x60000370c320; fillMode = backwards; timingFunction = default; keyPath = position; fromValue = NSPoint: {25, 325}>
(lldb) po ((CABasicAnimation *)action).toValue
nil
默认生成的action是一个 CABasicAnimation
,且该动画的fromValue为设置frame之前的值,而toValue为nil,这种情况动画如何播放在 CABasicAnimation
的官方文档中给出了处理方式:
/* The objects defining the property values being interpolated between.
* All are optional, and no more than two should be non-nil. The object
* type should match the type of the property being animated (using the
* standard rules described in CALayer.h). The supported modes of
* animation are:
*
* - both `fromValue' and `toValue' non-nil. Interpolates between
* `fromValue' and `toValue'.
*
* - `fromValue' and `byValue' non-nil. Interpolates between
* `fromValue' and `fromValue' plus `byValue'.
*
* - `byValue' and `toValue' non-nil. Interpolates between `toValue'
* minus `byValue' and `toValue'.
*
* - `fromValue' non-nil. Interpolates between `fromValue' and the
* current presentation value of the property.
*
* - `toValue' non-nil. Interpolates between the layer's current value
* of the property in the render tree and `toValue'.
*
* - `byValue' non-nil. Interpolates between the layer's current value
* of the property in the render tree and that plus `byValue'. */
在以上情况下,动画是由fromValue到当前值进行动画,当前值为多少呢:
(lldb) po self.frame
(origin = (x = 0, y = 300), size = (width = 50, height = 50))
由此可见,CALayer在修改UI信息时,UI信息会立刻生效,但会默认生成一个由设置前信息到设置后信息的动画,然后加入animation中,从而在下一次刷新时,presentationLayer能够以animation信息进行动画播放。
四、奇怪现象
在上述CALayer默认生成的Animation中,有一个特殊的情况:该Animation的duration为0:
(lldb) po ((CAAnimation *)action).duration
0
为什么这种情况还会播放动画,这是系统对CALayer的单独处理还是说对CAAnimation都有该种处理呢,我们写个例子看一下:
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position";
animation.fromValue = [NSValue valueWithCGPoint:self.animationView.center];
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(300, 300)];
animation.duration = 0;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[self.animationView.layer addAnimation:animation forKey:@"animation"];
运行效果如下:
由此看出,普通的CAAnimation也会有此种情况,这是为什么呢?
我们知道,当把CAAnimation提交至CALayer后,在屏幕刷新时,渲染层会由一个 CATransaction
来统一管理所有CALayer的animation,若无任何人为代码介入,此时提交到渲染层的动画我们就会称之为隐式动画,在 CATransaction
的官方文档中,有如下介绍:
/* Accessors for the "animationDuration" per-thread transaction
* property. Defines the default duration of animations added to
* layers. Defaults to 1/4s. */
+ (CFTimeInterval)animationDuration;
由此可见,即使Aniamtion的时间设定为0,系统渲染层也会为该动画执行 1/4s
时间的动画。
五、UIView无动画的原因
咱们回到最初的问题,现在已经知道所谓的隐式动画及为单个CALayer改变UI信息时会播放动画的原因,那么为什么改变UIView或UIView的layer的UI信息缺不播放动画呢?
CALayer播放动画是在 -actionForKey:
流程末端生成了animation,但UIView的layer会默认将delegate设置为UIView,那么UIView有做什么处理吗?我们hook看看:
+ (void)load
{
[self swizzleInstanceMethod:@selector(actionForLayer:forKey:) withMethod:@selector(sg_actionForLayer:forKey:)];
}
- (id<CAAction>)sg_actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [self sg_actionForLayer:layer forKey:event];
return action;
}
断点打印:
(lldb) po action
<null>
由此可见,UIView在CALayer获取action流程中第一步中,就将之后的流程阻断了,导致CALayer无法在末端生成action,从而屏蔽动画。
六、UIView动画
至此,我们知道了直接为CALayer设置UI信息会触发动画,直接为UIView或UIView的layer设置UI信息不会触发动画,那么UIView的 animateWithDuration:animations:
系列方法播放动画的原理是什么呢?
既然通过UIView的动画方法可以播放动画,其实归根结底还是为UIView的layer添加了animation,那我们通过自定义UIView和CALayer,然后重写CALayer的 addAnimation:forKey:
方法来查看动画如何被添加:
0 ??? 0x000000010a2b2937 0x0 + 4465568055,
1 ViewTest 0x0000000107e129a0 main + 0,
2 UIKitCore 0x00007fff47cf362f __67-[_UIViewAdditiveAnimationAction runActionForKey:object:arguments:]_block_invoke.211 + 1300,
3 UIKitCore 0x00007fff47cf2eb4 -[_UIViewAdditiveAnimationAction runActionForKey:object:arguments:] + 3063,
4 QuartzCore 0x00007fff2b14407b _ZN2CA5Layer12set_positionERKNS_4Vec2IdEEb + 333,
5 QuartzCore 0x00007fff2b132460 -[CALayer setPosition:] + 49,
6 QuartzCore 0x00007fff2b132b05 -[CALayer setFrame:] + 563,
7 UIKitCore 0x00007fff47d1665b -[UIView(Geometry) setFrame:] + 482,
8 ViewTest 0x0000000107e1231f __29-[ViewController viewDidLoad]_block_invoke_2 + 191,
9 UIKitCore 0x00007fff47d2862a +[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 528,
10 UIKitCore 0x00007fff47d28c48 +[UIView(UIViewAnimationWithBlocks) animateWithDuration:animations:completion:] + 86,
11 ViewTest 0x0000000107e12244 __29-[ViewController viewDidLoad]_block_invoke + 148,
12 libdispatch.dylib 0x0000000108139d48 _dispatch_client_callout + 8,
13 libdispatch.dylib 0x000000010813c6ba _dispatch_continuation_pop + 552,
14 libdispatch.dylib 0x000000010814fa0f _dispatch_source_invoke + 2205,
15 libdispatch.dylib 0x0000000108147c1d _dispatch_main_queue_callback_4CF + 1043,
16 CoreFoundation 0x00007fff23bb1df9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9,
17 CoreFoundation 0x00007fff23baca59 __CFRunLoopRun + 2329,
18 CoreFoundation 0x00007fff23babe16 CFRunLoopRunSpecific + 438,
19 GraphicsServices 0x00007fff38438bb0 GSEventRunModal + 65,
20 UIKitCore 0x00007fff4784fb48 UIApplicationMain + 1621,
21 ViewTest 0x0000000107e12a14 main + 116,
22 libdyld.dylib 0x00007fff51a1dc25 start + 1,
23 ??? 0x0000000000000001 0x0 + 1
由调用栈可知,在UIView动画方法的animations:参数中,当改变UI信息时,会由一个 _UIViewAdditiveAnimationAction
调用 CAAction
的代理方法生成一个 CAAnimation
对象,该对象数据如下:
(lldb) po anim
<CABasicAnimation:0x600002cad7a0; toValue = NSPoint: {0, 0}; additive = 1; delegate = <UIViewAnimationState: 0x7fd114d0c720>; fillMode = both; timingFunction = easeInEaseOut; duration = 2; fromValue = NSPoint: {-200, 0}; keyPath = position>
由打印可见,该动画与前面提到的默认动画有点出入,它的fromValue与toValue是以UIView设置后位置的bounds为准的,这样做会保证UIView在动画后响应范围的正确性。
既然动画是由Action生成的,那么我们可以通过重写UIView的 actionForLayer:forKey:
方法来查看该Action是否是由UIView生成的:
(lldb) po action
<_UIViewAdditiveAnimationAction: 0x600001b2c020>
至此,我们可以得知,在UIView的动画方法animations:参数中更改的UI信息不会被UIView的 actionForLayer:forKey:
方法阻断,反而会生成一个 _UIViewAdditiveAnimationAction
私有类对象,进而提供一个对应的 CAAnimation
对象,实现动画的播放。
七、参考资料
- CALayer的模型层与展示层 (强烈建议阅读)