项目常见崩溃4(陆续更新)

前情提要, 上回书说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我想就不会有这种崩溃了, 但是, 显然大部分开发者都是不清楚的.

你可能感兴趣的:(项目常见崩溃4(陆续更新))