浅谈CALayer隐式动画

说在前头

对于许多使用过CAAnimation的开发者来说,“隐式动画”的概念应该并不陌生,简单地说,我们直接在代码中设置如下属性:

1
2
3
4
5
layer.backgroundColor = newColor.cgColor
layer.hidden = !layer.hidden
layer.bounds = newRect

...

系统将会默认创建一段动画,来animate对应属性的变化。

若想禁用或自定义这段动画的参数,可以调用CATransaction类方法来实现自定义,当然,如果想要更完整的控制权,也可以创建一个CAAnimation的实例。

这玩意我知道

是的,我们都对隐式动画有所了解,很多时候,隐式动画使得界面的更新不那么突兀。然而由于习惯了使用UIView对CALayer的封装,我在上周的开发中遇到了如下的问题:

项目中需要实现类似“飘赞”功能,当用户点击屏幕时,创建一个新的视图,播放一段帧动画,然后隐藏,由于用户可以快速点击屏幕,为了提升性能,我想到利用CALayer来进行动画的播放,并且使用一定的复用机制,这样可以节省分配/销毁内存的开销,也避免内存过度增长,代码大概长这样:

动画Layer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@class AnimationLayer;
@protocol AnimationLayerDelegate <NSObject>

-(void)animationDidFinish:(AnimationLayer *)animLayer;

@end

@interface AnimationLayer : CALayer <CAAnimationDelegate>

-(void)animate;
-(void)clean;

@property (nonatomic ,weak)id <AnimationLayerDelegate> animDelegate;

@end

@implementation AnimationLayer

-(void)animate{
self.opacity = 1.0;
CABasicAnimation *anim = ...;

anim.delegate = self;

[self addAnimation:anim forKey:nil];
}

-(void)clean{
self.opacity = 0.0;
self.transform = CATransform3DIdentity;
...
}

-(void)dealloc{
NSLog(@"dealloc");
}


-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
[_animDelegate animationDidFinish:self];
}


控制器复用layer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

-(void)addAnimation{
if (self.layers.count > 0) {
AnimationLayer *l = self.layers[0];
[self.layers removeObjectAtIndex:0];
[l animate];
NSLog(@"Reuse Layer");
return ;
}

AnimationLayer *l = [AnimationLayer new];
l.bounds = CGRectMake(0, 0, 50, 50);
l.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:l];
l.animDelegate = self;

NSLog(@"Create New Layer");
[l animate];
}

-(void)animationDidFinish:(AnimationLayer *)animLayer{
[animLayer clean];
if (self.layers.count > MAX_REUSE_LIMIT) {
[animLayer removeFromSuperlayer];
}
else{
NSLog(@"Add Layer To Reuse Queue");

[self.layers addObject:animLayer];
}
}

上面的代码为Layer设置了一个AnimationDelegate,以便在动画结束后,视情况加入复用队列中,这样成功的减少了alloc/dealloc的次数,也免去了部分对于layer层级树的操作,接下来让程序跑起来吧!

在经过几次简单的测试之后,我发现了如下的控制台输出:

1
2
3
4
5
6
7
8
9
10
11
LayerTest[1258:66921] Create New Layer
LayerTest[1258:66921] Add Layer To Reuse Queue
LayerTest[1258:66921] dealloc
LayerTest[1258:66921] Reuse Layer
LayerTest[1258:66921] dealloc
LayerTest[1258:66921] Add Layer To Reuse Queue
LayerTest[1258:66921] dealloc
LayerTest[1258:66921] Reuse Layer
LayerTest[1258:66921] dealloc
LayerTest[1258:66921] Add Layer To Reuse Queue
LayerTest[1258:66921] dealloc

Layer的创建,加入复用队列以及出队复用逻辑都是正确的,但是这么多的dealloc是什么情况?

隐式动画的实质

在一番探索之后,我才终于弄清楚这个被dealloc的对象到底是怎么被创建出来的:

从调用堆栈中,很容易可以看出,在我们的[AnimationLayer clean]方法中,setOpacity:触发了Implict Animation(不得不说苹果这命名还是很直观的),而在调用CALayerpresnetation_layer时候,系统调用了initWithLayer:方法,来创建一个与目标layer相同的layer来实现动画效果。

结合这篇文章,我们就能更好理解在设置属性时,UIViewCALayer为什么会有如此不同的表现了。

总结

UIView是对CALayer的封装,为我们提供了许多更便利的接口,但对于动画更多细致的控制,或许还是要通过Core Animation来实现,而这其中由于“便利”的UIView导致的可能出现的疏忽,还有待我慢慢发现。