问题描述
今天遇到一个基本问题,那就是:假设A为rootViewController,在适当的时机使用A present 另一个viewController B。然后会发现写在B viewDidLoad中的动画不会执行。
解决办法
- 方法一:将动画写在viewWillAppear或者之后执行的方法中,但是需要注意该方法会多次执行。
- 方法二:将动画的removedOnCompletion属性设置为NO。
类似问题(切换后台导致动画失效)
在点击home键切换到后台,然后在切换回来时,会发现动画都无效了。解决办法与上面的类似:
- 方法一:设置观察者在app将要进入前台时重置动画。
- 方法二:将动画的removedOnCompletion属性设置为NO。
总结
相信在开发中大多数人会遇到描述的第二个问题;而遇到present后动画失效这个问题的人应该要少一些,因此搜索引擎上也就十分罕见,所以我把它记录在此。至此问题就解决了,下面将要写的是我是如何定位到这个问题的,如果您的时间宝贵,那么可以略过下面的篇幅。
实际遇到的问题和排查过程
问题描述
上面描述到的问题是我最后简略所总结出的。实际的问题是:同事在一个view的初始化过程中添加了一个动画(CoreAnimation),而这个动画不执行了。
分析过程
- 这个时候大家的第一反应肯定是动画是不是写的有问题?于是我在原来加动画的地方写出了另一个动画(确定正确的)然后 command + R 发现然并卵。ps:同事也是试验过其他动画的。
- 接着当然就是短暂的懵逼咯,仔细回想是不是忘记了什么,毕竟很久没有写动画相关的代码了,但是怎么想都想不出是为什么。
- 接下来想到是不是viewController哪里出了问题,于是找到了viewController的实现代码看了看,嗯,没什么问题。又看了初始化的时候,简单的alloc、init似乎也没什么问题。
- 那么问题出在哪里呢?然后我注意到了这个viewController是present出去的,难道说是这个原因?恰巧这是一个可以Push的视图控制器(拥有navigationController),于是改用push方法,发现添加的动画可以执行了!
- 但是我还是不知道到底是为什么present的vc不执行动画啊,接下来我直接将一个新创建的viewController(无多余的代码),设置为rootViewController,在这个rootViewController中present出上面描述的含有动画的视图控制器,发现动画还是不执行。
- 我又重新创建了一个视图控制器,在viewDidLoad中加入了相关的动画,然后用rootViewController present它,接着运行工程,发现还是不执行。
- 为了彻底排除是工程中一些配置所引起的原因,只有重新起一个工程来测试,然而在新的工程中还是不执行,那么至此可以断定,是present导致的动画不执行。
- 问题找出来了,那么为什么?突然我想到,以前在做一个持续性动画的时候,发现切换到后台在切换回来后动画就不起作用了,最后经过搜索和验证发现是切换到后台时所有的动画被移除了,那么会不会present有类似的手法移除了动画呢?那怎么验证? 简单的方式就是用dispatch_after。在设置延迟之后,发现动画执行了。下面是代码验证过程:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor grayColor];
v = [[UIView alloc] initWithFrame:CGRectMake(0, 20, 100, 100)];
v.backgroundColor = [UIColor orangeColor];
[self.view addSubview:v];
CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
ani.fromValue = @0;
ani.delegate = self;
ani.toValue = @M_PI;
ani.duration = 2.0;
ani.repeatCount = HUGE_VALF;
[v.layer addAnimation:ani forKey:@"opm"];
NSLog(@"%@",v);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%s %@",__func__,v);
}
上述代码中,在添加了动画之后打印了一次v,然后viewWillAppear中也打印了一次v(目的是看看有没有动画在其中),控制台的输出为:
del07-10[8293:207637] <UIView: 0x7f836fc0b710; frame = (0 20; 100 100); animations = { opm=<CABasicAnimation: 0x6000000340e0>; }; layer = <CALayer: 0x600000034240>>
del07-10[8293:207637] -[PViewController viewWillAppear:] <UIView: 0x7f836fc0b710; frame = (0 20; 100 100); layer = <CALayer: 0x600000034240>>
可以清楚的看到,在不加延迟时动画在viewWillAppear中就已经不存在了。
当为加动画的代码块设置延迟后,动画就正常执行了。
- 我就有点好奇,他是用什么方法移除所有的动画的,不会是用layer的removeAllAnimations方法吧?虽然自己都不相信苹果会用会用这个方法,但是好奇心太强,想试试,我想到两个试的办法:
一、写一个Layer继承自CALayer,然后把相关layer都换成这个类的。
二、Method Swizzling,利用runtime交换removeAllAnimations的实现。
自然而然地,我采用了第二个方法,因为它有逼格啊 O(∩_∩)O
为了保证一开始方法就被替换,我在appDelegate中加入了如下代码:
AppDelegate.m
- (void)exchangeMethod {
Method original = class_getInstanceMethod([CALayer class], @selector(removeAllAnimations));
Method custom = class_getInstanceMethod([AppDelegate class], @selector(customMethod));
method_exchangeImplementations(original, custom);
}
- (void)customMethod {
NSLog(@"%s开始",__func__);
NSLog(@"self=%@",self);
NSLog(@"%s结束",__func__);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[self exchangeMethod];
return YES;
}
经过以上代码,我期待,在present的时候会打印出出我想要的东西,事实就是并没有打印,既然一开始就料到了苹果不会采用这么“低端”的方法来移除动画,所以也没什么好伤心的。写都写了,不如试试切换后台呢?
我把Present 到的那个viewController做了下如修改(经过一段延迟之后再加动画),使动画能够运行:
//关键代码
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
v = [[UIView alloc] initWithFrame:CGRectMake(0, 20, 100, 100)];
v.backgroundColor = [UIColor orangeColor];
[self.view addSubview:v];
CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
ani.fromValue = @0;
ani.delegate = self;
ani.toValue = @M_PI;
ani.duration = 2.0;
ani.repeatCount = HUGE_VALF;
[v.layer addAnimation:ani forKey:@"opm"];
});
等待动画执行后,我把app切换到后台,控制台输出:
del07-10[9174:251348] -[AppDelegate customMethod]开始
del07-10[9174:251348] self=<CALayer: 0x600000031280>
del07-10[9174:251348] -[AppDelegate customMethod]结束
很令人激动啊,居然执行了,也就是说调用了removeAllAnimations方法,如果有童鞋不理解为什么这里输出的self是CALayer而不是Appdelegate,那么你们理解runtime的消息发送机制后应当会理解,这里就不多说了。
- 既然证明调用了,那么就可以说,切换后台和prensent他们清楚动画的方式不一致。
仔细推敲会发现,上面那句话是错的,因为此时此刻我们并不知道这个Layer是属于谁的。然后我尝试打印出可见view以及superview中的layer,想要看看是哪个layer调用了移除动画的方法。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
v = [[UIView alloc] initWithFrame:CGRectMake(0, 20, 100, 100)];
v.backgroundColor = [UIColor orangeColor];
[self.view addSubview:v];
CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
ani.fromValue = @0;
ani.delegate = self;
ani.toValue = @M_PI;
ani.duration = 2.0;
ani.repeatCount = HUGE_VALF;
[v.layer addAnimation:ani forKey:@"opm"];
UIView *x = v;
while (x) {
NSLog(@"Layer寻找:%@",x.layer);
x = x.superview;
}
});
重新运行,等待动画执行,切到后台。控制台输出如下:
del07-10[9362:256937] Layer寻找:<CALayer: 0x60000003ac80>
del07-10[9362:256937] Layer寻找:<CALayer: 0x60800003d240>
del07-10[9362:256937] Layer寻找:<CALayer: 0x60800003fde0>
del07-10[9362:256937] Layer寻找:<UIWindowLayer: 0x60800003cb80>
del07-10[9362:256937] -[AppDelegate customMethod]开始
del07-10[9362:256937] self=<CALayer: 0x60800022d240>
del07-10[9362:256937] -[AppDelegate customMethod]结束
经过多次的试验,发现没能找到与调用removeAllAnimations layer相同的layer。很失望啊!
然后各种折腾,最后静下心来,在断点调试中看到了如下信息:
<CALayer:0x60000003ae40; position = CGPoint (0 0); bounds = CGRect (0 0; 0 0); delegate = <UIKeyboardImpl: 0x7f84686238e0; frame = (0 0; 0 0); layer = <CALayer: 0x60000003ae40>>; opaque = YES; allowsGroupOpacity = YES; >
也就是说这个调用removeAllAnimations的layer可能是和键盘的动画有关,虽然我不知道UIKeyboardImpl是个什么东东。
- 然后我才想起用真机跑一下呢,什么代码都不加,真机里面,切换到后台,没有任何一个layer调用removeAllAnimations。
- 接着,我在界面上增加了一个UITextField,运行后点击UITextField让键盘弹出,然后切换到后台。
控制台输出如下:
del07-10[1911:634198] Layer寻找:<CALayer: 0x17403b500>
del07-10[1911:634198] Layer寻找:<CALayer: 0x17402d340>
del07-10[1911:634198] Layer寻找:<CALayer: 0x17002f900>
del07-10[1911:634198] Layer寻找:<UIWindowLayer: 0x17002ec40>
del07-10[1911:634198] -[AppDelegate customMethod]开始
del07-10[1911:634198] self=<CALayer: 0x17403df20>
del07-10[1911:634198] -[AppDelegate customMethod]结束
del07-10[1911:634198] -[AppDelegate customMethod]开始
del07-10[1911:634198] self=<CALayer: 0x17403dfc0>
del07-10[1911:634198] -[AppDelegate customMethod]结束
del07-10[1911:634198] -[AppDelegate customMethod]开始
del07-10[1911:634198] self=<CALayer: 0x17403df40>
del07-10[1911:634198] -[AppDelegate customMethod]结束
...//若干次
也就是说我们试验到的调用removeAllAnimations的layer仅仅是和键盘相关的。
ps:在尝试的过程中,也怀疑过是否是present的转场动画会与我们写的冲突,所以系统才移除,尝试时不仅仅将animated设置为了NO,还使用了自定义的转场动画,发现都是不能执行的。
综述
经过这么一番折腾,我们可以知道的是当present一个viewController时,系统会移除该viewController的动画;当切换到后台是,系统也会移除当前动画。