前情提要, 上回書說UIAlertView會在iOS9以下的系統(tǒng)上產(chǎn)生內(nèi)存泄露, 解決方案是要么退出VC的時候直接關(guān)閉alertView要么全局控制alertView. 上回在講到關(guān)閉alertView的時候談到了崩潰, 今天重點說下UIAlertView退出時候的崩潰.
崩潰重現(xiàn)
顯示alertView
UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@""
message:@"您的號在另一終端登錄"
delegate:self
cancelButtonTitle:@"確定"
otherButtonTitles:@"取消", nil];
[alertView show];
self.alertView = alertView;
[self.navigationController popViewControllerAnimated:NO];
這里顯示之后用一個強指針指著, 防止直接釋放, 然后立即pop掉當(dāng)前controller, 并在dealloc中執(zhí)行如下方法
- (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一個已經(jīng)在過渡的VC了, 這個VC是_UIAlertControllerAlertPresentationController
, 貌似有點眼熟, _UIAlertControllerShimPresenterWindow
上回提到UIAlertView被添加的window就是這個window, 那看這架勢_UIAlertControllerAlertPresentationController
必然是這個window內(nèi)的一個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]));
結(jié)果是:
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. 繼續(xù)說崩潰, 崩潰發(fā)生在iOS8.X系統(tǒng)上, 來看看iOS9以上運行的結(jié)果
結(jié)果是不會崩潰, 而且是在VC被干掉的時候也關(guān)閉了UIAlertView, 這不正好是我們想要的結(jié)果嗎, 實際上iOS9以上已經(jīng)做到了, 而且我們的代碼一點都沒變啊! 良心企業(yè)有木有, 但是我們還是要兼容iOS8, 因為iOS現(xiàn)在出到10, 一般的公司都會兼容3個版本, 所以iOS8上的崩潰那也是崩潰, 還是要處理的.
矛盾
對于iOS8.X系統(tǒng)如果不在dealloc的時候dismiss會有內(nèi)存泄露, 如果dismiss就可能會崩潰, 這可如何是好啊! 從崩潰日志上看, alertView在顯示的時候是創(chuàng)建了一個VC的, 然后這個VC是通過pressent的方式添加到當(dāng)前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開始的時候去設(shè)置present的狀態(tài), 結(jié)束后重置狀態(tài)并拋通知, 這個通知在你的自定義導(dǎo)航控制器中監(jiān)聽
- (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];
}
這樣當(dāng)你進行pop操作的時候先看下是否有正在presenting, 如果有就監(jiān)聽這個結(jié)束通知, 等結(jié)束后再繼續(xù)執(zhí)行, 這里只是簡單的代碼, 實際pop的時候還要進行其它的判斷, 用隊列比較合適, 這里為了簡單說明問題, 只是簡單處理下, 方便讀者閱讀.
好了, 到這里應(yīng)該就沒崩潰了, 我們再重新驗證下下面幾個問題
1 還有內(nèi)存泄露嗎?
2 還崩潰嗎?
3 在其它系統(tǒng)上有沒有問題?
答案是:
1 還有內(nèi)存泄露(這里真的不知道為什么會這樣, 只能如實稟告大家)
2 沒有崩潰了
3 在其它機器上是沒有問題的, 沒內(nèi)存泄露也沒有崩潰, 異常日志都沒有.
這里還是有一個關(guān)于邏輯的問題, 如果有彈窗, 那就不允許pop還是等彈窗出來再pop, 實際上, 大部分情況是, 彈窗都是會指定頁面的, 只有被強制退出這種彈窗才會伴隨pop, 而這種彈窗的數(shù)量應(yīng)該是很少的, 應(yīng)該是做成全局的. 對于頁面內(nèi)的彈窗, 個人感覺如果此時恰好要pop是可以選擇不進行pop的, 你可以繼續(xù)操作彈窗.
總結(jié)
對一個廢棄的東西UIAlertView說了這么多, 總結(jié)一下這里涉及到很多我們平時不太關(guān)注的東西, 作為一個開發(fā), 應(yīng)該是要了解更多的, 不能只是會用.
1 AppDelegate的window和keyWindow不一樣, keyWindow是當(dāng)前正在顯示的window, 而AppDelegate里的window是我們應(yīng)用真切能夠用到的window
2 UIAlertView實際通過present的方式添加到keyWindow上, 在present的時候進行去執(zhí)行dismiss是會崩潰的, 這點即使使用UIAlertController也是需要注意的.
3 在有present進行的時候盡量不要進行push或者pop造作, 因為這樣很容易導(dǎo)致一些UI的釋放操作, 比如dismiss, 進而可能會崩潰.
4 許多詭異的崩潰可能往往就是我們不懂其中的原理導(dǎo)致的, 如果大家都知道UIAlertView是通過present方式添加并且present不允許dismiss我想就不會有這種崩潰了, 但是, 顯然大部分開發(fā)者都是不清楚的.