問題描述
今天遇到一個基本問題,那就是:假設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的動畫;當切換到后臺是,系統也會移除當前動畫。