iOS开发中,UIViewController是最经常使用的一个类,在Push和Pop的过程当中也会常常出现一些UI卡死、App闪退的问题,本文总结了开发中遇到的一些坑。app
大部分视图控制器切换致使的问题,根本缘由都是使用了动画,由于执行动画须要时间,在动画未完成的时候又进行另外一个切换动画,容易产生异常,假如在 Push 和 Pop 的过程不使用动画,世界会清静不少。因此本文只讨论使用了动画的视图切换。也就是使用如下方式的 Push 和 Pop:ide
self.navigationController pushViewController:controller animated:YES]; [self.navigationController popViewControllerAnimated:YES];
连续两次 Push 不一样的 ViewController 是没问题的,好比这样:oop
- (void)onPush: { [self.navigationController pushViewController:vc1 animated:YES]; [self.navigationController pushViewController:vc2 animated:YES]; }
可是,若是不当心连续 Push 了同一个 ViewController,而且 animated 为 YES,则会 Crash:Pushing the same view controller instance more than once is not supported
。测试
这种状况颇有可能发生,特别是界面上触发切换的入口不止一处,而且各个入口的点击没有互斥的话,用两根手指同时点击屏幕就会同时触发两个入口的切换了。多点触碰致使的同时 Push,基本上是防不胜防,当界面元素很复杂的时候,特别容易出现这个问题,而期望从用户交互的角度上避免这个问题是不可能的,测试美眉以暴力测试、胡乱点击而著称,防得了用户防不住测试。动画
因此咱们须要从根本上解决这个问题:当一个 Push 动画还没完成的时候,不容许再 Push 别的 ViewController。这样处理是没有问题的,由于连续带动画地 Push 多个 ViewController 确定不是开发和产品的意愿,就算有这种需求,也能够经过禁用动画的方式来解决。atom
继承 UINavigationController 并重载 pushViewController 方法。spa
isSwitching == YES
,则忽略此次 Push。isSwitching = YES
再继续切换。isSwitching
改成 NO。@interface MYNavigationController () <UINavigationControllerDelegate, UIGestureRecognizerDelegate> @property (assign, nonatomic) BOOL isSwitching; @end @implementation MYNavigationController // 重载 push 方法 - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { if (animated) { if (self.isSwitching) { return; // 1. 若是是动画,而且正在切换,直接忽略 } self.isSwitching = YES; // 2. 不然修改状态 } [super pushViewController:viewController animated:animated]; } #pragma mark - UINavigationControllerDelegate - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { self.isSwitching = NO; // 3. 还原状态 }
连续 Pop ,可能会致使两种状况。3d
例如,下面的代码,执行到第二句的时候,self 已经被释放了。code
[self.navigationController popViewControllerAnimated:YES]; // self 被 release [self.navigationController popViewControllerAnimated:YES]; // 继续访问 self 致使异常
假如你避开了上面那种调用,换成了这样:继承
[[AppDelegate sharedObject].navigationController popViewControllerAnimated:YES]; [[AppDelegate sharedObject].navigationController popViewControllerAnimated:YES];
因为访问的是全局的 AppDelegate,天然避免了调用者被释放的问题,可是,连续两次动画 Pop,在iOS 7.X 系统会致使界面混乱、卡死、莫名其妙的崩溃(iOS 8 貌似不存在相似的问题)。好比,下面这个崩溃的堆栈:
{"bundleID":"com.enterprise.kiwi","app_name":"kiwi","bug_type":"109","name":"kiwi","os_version":"iPhone OS 7.1.1 (11D201)","version":"1190 (3.1.0)"} Incident Identifier: FE85E864-393C-417D-9EA0-B4324BEEDA2F CrashReporter Key: a54805586b9487c324ff5f42f4ac93dabbe9f23e Hardware Model: iPhone6,1 Process: kiwi [1074] Path: /var/mobile/Applications/D81CE836-3F88-481C-AA5A-21DA530234E0/kiwi.app/kiwi Identifier: com.yy.enterprise.kiwi Version: 1190 (3.1.0) Code Type: ARM-64 (Native) Parent Process: launchd [1] Date/Time: 2015-09-08 15:44:57.327 +0800 OS Version: iOS 7.1.1 (11D201) Report Version: 104 Exception Type: EXC_CRASH (SIGSEGV) Exception Codes: 0x0000000000000000, 0x0000000000000000 Triggered by Thread: 1 Thread 0: 0 libobjc.A.dylib 0x00000001993781dc objc_msgSend + 28 1 UIKit 0x000000018feacf14 -[UIResponder(Internal) _canBecomeFirstResponder] + 20 2 UIKit 0x000000018feacba0 -[UIResponder becomeFirstResponder] + 240 3 UIKit 0x000000018feacfa0 -[UIView(Hierarchy) becomeFirstResponder] + 120 4 UIKit 0x000000018ff320f8 -[UITextField becomeFirstResponder] + 64 5 UIKit 0x000000018ffe4800 -[UITextInteractionAssistant(UITextInteractionAssistant_Internal) setFirstResponderIfNecessary] + 208 6 UIKit 0x000000018ffe3f84 -[UITextInteractionAssistant(UITextInteractionAssistant_Internal) oneFingerTap:] + 1792 7 UIKit 0x000000018ffcac60 _UIGestureRecognizerSendActions + 212 8 UIKit 0x000000018fe5929c -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 376 9 UIKit 0x000000019025803c ___UIGestureRecognizerUpdate_block_invoke + 56 10 UIKit 0x000000018fe1a258 _UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks + 284 11 UIKit 0x000000018fe18b34 _UIGestureRecognizerUpdate + 208 12 UIKit 0x000000018fe57b1c -[UIWindow _sendGesturesForEvent:] + 1008 13 UIKit 0x000000018fe5722c -[UIWindow sendEvent:] + 824 14 UIKit 0x000000018fe28b64 -[UIApplication sendEvent:] + 252 15 UIKit 0x000000018fe26c54 _UIApplicationHandleEventQueue + 8496 16 CoreFoundation 0x000000018ce1f640 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 20 17 CoreFoundation 0x000000018ce1e99c __CFRunLoopDoSources0 + 252 18 CoreFoundation 0x000000018ce1cc34 __CFRunLoopRun + 628 19 CoreFoundation 0x000000018cd5dc1c CFRunLoopRunSpecific + 448 20 GraphicsServices 0x0000000192a45c08 GSEventRunModal + 164 21 UIKit 0x000000018fe8efd8 UIApplicationMain + 1152 22 kiwi 0x000000010026a2b8 main (main.mm:26) 23 libdyld.dylib 0x000000019995ba9c start + 0 Thread 1 Crashed: 0 libsystem_kernel.dylib 0x0000000199a3daa8 kevent64 + 8 1 libdispatch.dylib 0x0000000199941998 _dispatch_mgr_thread + 48
从崩溃记录彻底看不出缘由,十分坑爹。
Push 的过程当中调用 Pop,会致使界面卡死,表现为:不响应任何点击、手势操做,可是不会崩溃。这也是在 iOS7 中出现的问题,iOS 8 以后不存在。
同 1.1,重载 Pop 方法:
#pragma mark - UINavigationController - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated { if (!self.isSwitching) { return [super popToViewController:viewController animated:animated]; } else { [self enqueuePopViewController:viewController animate:animated]; return nil; } } - (UIViewController *)popViewControllerAnimated:(BOOL)animated { if (!self.isSwitching) { return [super popViewControllerAnimated:animated]; } else { [self enqueuePopViewController:nil animate:animated]; return nil; } } #pragma mark - UINavigationControllerDelegate - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { self.isSwitching = NO; // 显示完毕以后判断是否须要Pop if (self.popVCAnimateQueue.count) { PopVCInfo *info = [self.popVCAnimateQueue firstObject]; [self.popVCAnimateQueue removeObjectAtIndex:0]; if (info.controller) { [self.navigationController popToViewController:info.controller animated:info.animate]; } else { [self.navigationController popViewControllerAnimated:info.animate]; } } }
手势滑动返回本质上调用的仍是 Pop,因此,同上。
不过,还能够更根本地禁止用户进行这样的操做,也就是在切换过程当中禁止滑动返回手势。
#pragma mark - UINavigationController // Hijack the push method to disable the gesture - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { self.interactivePopGestureRecognizer.enabled = NO; [super pushViewController:viewController animated:animated]; } #pragma mark - UINavigationControllerDelegate - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { self.isSwitching = NO; self.interactivePopGestureRecognizer.enabled = YES; // 显示完毕以后判断是否须要Pop if (self.popVCAnimateQueue.count) { PopVCInfo *info = [self.popVCAnimateQueue firstObject]; [self.popVCAnimateQueue removeObjectAtIndex:0]; if (info.controller) { [self.navigationController popToViewController:info.controller animated:info.animate]; } else { [self.navigationController popViewControllerAnimated:info.animate]; } } }