前情提要, 上回书说UIAlertView会在iOS9以下的系统上产生内存泄露, 解决方案是要么退出VC的时候直接关闭alertView要么全局控制alertView. 上回在讲到关闭alertView的时候谈到了崩溃, 今天重点说下UIAlertView退出时候的崩溃.
崩溃重现
显示alertView
UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@""
message:@"您的号在另一终端登录"
delegate:self
cancelButtonTitle:@"确定"
otherButtonTitles:@"取消", nil];
[alertView show];
self.alertView = alertView;
[self.navigationController popViewControllerAnimated:NO];
这里显示之后用一个强指针指着, 防止直接释放, 然后立即pop掉当前controller, 并在dealloc中执行如下方法
- (void)dealloc {
[self.alertView dismissWithClickedButtonIndex:0 animated:YES];
}
好么, 这不就是上回书说的方案1吗?怎么还会崩溃!
先看下崩溃日志:
2017-01-18 09:28:59.667 28-UIAlertViewCrash[1023:16310] Trying to dismiss the presentation controller while transitioning already. (<_UIAlertControllerAlertPresentationController: 0x7874eab0>)
2017-01-18 09:28:59.668 28-UIAlertViewCrash[1023:16310] transitionViewForCurrentTransition is not set, presentation controller was dismissed during the presentation? (<_UIAlertControllerAlertPresentationController: 0x7874eab0>)
WTF, 你试图dismiss一个已经在过渡的VC了, 这个VC是_UIAlertControllerAlertPresentationController
, 貌似有点眼熟, _UIAlertControllerShimPresenterWindow
上回提到UIAlertView被添加的window就是这个window, 那看这架势_UIAlertControllerAlertPresentationController
必然是这个window内的一个controller了, 为了验证我们的说法, 打印下面的日志
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
NSLog(@"keywindow:%p:%@", keyWindow, NSStringFromClass([keyWindow class]));
UIViewController *viewController = keyWindow.rootViewController;
NSLog(@"keywindow's root vc:%p:%@", viewController, NSStringFromClass([viewController class]));
结果是:
2017-01-18 09:38:29.478 28-UIAlertViewCrash[1086:21182] keywindow:0x7b94a900:_UIAlertControllerShimPresenterWindow
2017-01-18 09:38:29.478 28-UIAlertViewCrash[1086:21182] keywindow's root vc:0x7b839ba0:_UIAlertShimPresentingViewController
没错了, _UIAlertControllerAlertPresentationController
就是_UIAlertControllerShimPresenterWindow
的rootViewController. 继续说崩溃, 崩溃发生在iOS8.X系统上, 来看看iOS9以上运行的结果
结果是不会崩溃, 而且是在VC被干掉的时候也关闭了UIAlertView, 这不正好是我们想要的结果吗, 实际上iOS9以上已经做到了, 而且我们的代码一点都没变啊! 良心企业有木有, 但是我们还是要兼容iOS8, 因为iOS现在出到10, 一般的公司都会兼容3个版本, 所以iOS8上的崩溃那也是崩溃, 还是要处理的.
矛盾
对于iOS8.X系统如果不在dealloc的时候dismiss会有内存泄露, 如果dismiss就可能会崩溃, 这可如何是好啊! 从崩溃日志上看, alertView在显示的时候是创建了一个VC的, 然后这个VC是通过pressent的方式添加到当前keywindow上的, 那么我们尝试hook presentViewController
代码如下
NSString* const finishPresenting = @"finishPresenting";
@implementation UIViewController (hook)
+ (void)load {
Class class = [self class];
SEL oriSel = @selector(presentViewController:animated:completion:);
SEL newSel = @selector(myPresentViewController:animated:completion:);
Method fromMethod = class_getInstanceMethod(class, oriSel);
Method toMethod = class_getInstanceMethod(class, newSel);
if (!class_addMethod(class, oriSel, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}
- (void)myPresentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion {
self.presenting = YES;
NSLog(@"myPresentViewController called:%p: %@", viewControllerToPresent, NSStringFromClass([viewControllerToPresent class]));
__weak typeof(self) weak_self = self;
[self myPresentViewController:viewControllerToPresent animated:flag completion:^{
weak_self.presenting = NO;
if (completion) {
completion();
}
[[NSNotificationCenter defaultCenter] postNotificationName:finishPresenting object:nil];
}];
}
- (void)setPresenting:(BOOL)presenting {
objc_setAssociatedObject(self, @selector(isPresenting), @(presenting), OBJC_ASSOCIATION_ASSIGN);
}
- (BOOL)isPresenting {
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
@end
这样在present开始的时候去设置present的状态, 结束后重置状态并抛通知, 这个通知在你的自定义导航控制器中监听
- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
NSLog(@"popViewControllerAnimated:animated:%d", animated);
if ([UIApplication sharedApplication].keyWindow.rootViewController.isPresenting) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onFinishPresenting:) name:finishPresenting object:nil];
});
return nil;
}
return [super popViewControllerAnimated:animated];
}
- (void)onFinishPresenting:(NSNotification *)notification {
[self popViewControllerAnimated:NO];
}
这样当你进行pop操作的时候先看下是否有正在presenting, 如果有就监听这个结束通知, 等结束后再继续执行, 这里只是简单的代码, 实际pop的时候还要进行其它的判断, 用队列比较合适, 这里为了简单说明问题, 只是简单处理下, 方便读者阅读.
好了, 到这里应该就没崩溃了, 我们再重新验证下下面几个问题
1 还有内存泄露吗?
2 还崩溃吗?
3 在其它系统上有没有问题?
答案是:
1 还有内存泄露(这里真的不知道为什么会这样, 只能如实禀告大家)
2 没有崩溃了
3 在其它机器上是没有问题的, 没内存泄露也没有崩溃, 异常日志都没有.
这里还是有一个关于逻辑的问题, 如果有弹窗, 那就不允许pop还是等弹窗出来再pop, 实际上, 大部分情况是, 弹窗都是会指定页面的, 只有被强制退出这种弹窗才会伴随pop, 而这种弹窗的数量应该是很少的, 应该是做成全局的. 对于页面内的弹窗, 个人感觉如果此时恰好要pop是可以选择不进行pop的, 你可以继续操作弹窗.
总结
对一个废弃的东西UIAlertView说了这么多, 总结一下这里涉及到很多我们平时不太关注的东西, 作为一个开发, 应该是要了解更多的, 不能只是会用.
1 AppDelegate的window和keyWindow不一样, keyWindow是当前正在显示的window, 而AppDelegate里的window是我们应用真切能够用到的window
2 UIAlertView实际通过present的方式添加到keyWindow上, 在present的时候进行去执行dismiss是会崩溃的, 这点即使使用UIAlertController也是需要注意的.
3 在有present进行的时候尽量不要进行push或者pop造作, 因为这样很容易导致一些UI的释放操作, 比如dismiss, 进而可能会崩溃.
4 许多诡异的崩溃可能往往就是我们不懂其中的原理导致的, 如果大家都知道UIAlertView是通过present方式添加并且present不允许dismiss我想就不会有这种崩溃了, 但是, 显然大部分开发者都是不清楚的.