iOS CALayer动画原理分析

一、引出问题

在开始分析原理之前,我们先来看一个问题:

我们都知道 UIViewCALayer之间的关系,通俗的来说,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

但真实情况确实如下图所示:

真实情况

UIViewUIView的 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的入口是在 CALayeractionForKey:方法处,我们可以看一下这个方法执行了什么,官方文档如下:

/* 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协议的对象,寻找流程如下:

  1. 如果CALayer的delegate存在并实现了 actionForLayer:forKey:方法,那么调用该方法寻找;
  2. 从CALayer的actions字典中根据event为key值寻找;
  3. 从CALyaer的style层级中的actions字典中根据event为key值寻找;
  4. 调用类方法 +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对象,实现动画的播放。

七、参考资料

  1. CALayer的模型层与展示层 (强烈建议阅读)
发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/103412222