iOS的页面基本由UIViewController, UINavigationController完成,切换方式也基本是Present, Push,Pop等等。这些切换过程会遇到以下两种crash
1. Can't add self as subview
2. Attempt to present xx on yy whose view is not in the window hierarchy!
3. 快速点击两次按钮,连续push两三次
第一个问题的原因有两种,一种是[self addSubview:self]; 第二种是连续两次push,或者pop,这个在iOS 7下概率极高。
第二个问题由于当前的页面根本不在window的最顶层,你无法使用当前VC做操作。
问题的根源在于系统在做UI切换的时候,由于动画没有执行完毕,页面层级和状态都不正确,这个时候再次发起切换动画,就会造成紊乱,严重时会引发crash
针对这两个问题
延时解决方案
self.navigationController?.pushViewController(UIViewController(), animated: true)
//延迟执行
DispatchQueue.main.asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 0.3)) {
self.navigationController?.pushViewController(UIViewController(), animated: true)
}
弊端
这种延时的方法,偶尔用上一两处,可能可以解决问题,但是如果满大街都是这种使用方法,那么问题依然存在,因为大家都用延时,在一个时间线上,肯定会出现两个切换间隔时间不足的问题。
viewDidAppear增加变量控制
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated);
//增加切换控制
self.isAnimated = NO;
}
public func pushViewController(_ viewController:UIViewController, animated:Bool) {
if (self.topViewController.isAnimated == ture) {
return
}
//省略
}
弊端
1.每一个VC增加一个类似的变量,需要在基类中维护
2.viewDidload被调用了,也不代表能push或者pop完成,真正完成切换在navigationController的代理didShow函数里面
3.没有做VC是否在Window 检测,依然会导致Crash
终极解决方案,UIWindow控制
问题的本质在于,同一时间一个UIWindow只能有一个页面切换,那么我们索性给UIWindow上增加一个控制变量
extension UIWindow {
//动画标志状态
var isAnimated: Bool {
get{
if objc_getAssociatedObject(self, &isAnimatedKey) != nil {
return (objc_getAssociatedObject(self, &isAnimatedKey) as? Bool)!
}
return false
}
set{
objc_setAssociatedObject(self, &isAnimatedKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
}
}
}
在每次切换的 加锁 变量, 弄完之后 释放 变量
切换前的需要处理的方法如下
UINavigationController,push pop, popTo 等等,先用方法替换的方式,
UIViewController做present 和dismiss方法处
将UINavigationController的方法做替换,这样可以便于
//UINavigationController
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "pushViewController:animated:", currentMethodName: "skipControlPushViewController:animated:")
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "popViewControllerAnimated:", currentMethodName: "skipControlPopViewControllerAnimated:")
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "popToViewController:animated:", currentMethodName: "skipControlPopToViewController:animated:")
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "popToRootViewControllerAnimated:", currentMethodName: "skipControlPopToRootViewControllerAnimated:")
//present push
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "presentViewController:animated:completion:", currentMethodName: "skipControlPresentViewController:animated:completion:")
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "dismissViewControllerAnimated:completion:", currentMethodName: "skipControlDismissViewControllerAnimated:completion:")
然后我们在替换的方法中将具体的跳转转接到一个单例中去,让他去负责做 加锁 和释放window 操作,然后跳转,例如在pushViewController中做法如下
public func skipControlPushViewController(_ viewController:UIViewController, animated:Bool) {
// 由于UINavigationController initWithRootViewController 会调用该方法,并且当时没有显示在Window上,所以特殊处理,此处不加控制,并不会造成crash
if self.viewControllers.count == 0 && animated == false {
self.skipControlPushViewController(viewController, animated: animated)
return
}
具体的 加锁 和释放Window的地方我们用了一个单例,而没有在push发发中执行 window. isAnimated = true? 先看看我们释放的window的地方在哪里,就明白为什么不这样做。
UINavigationController 切换完成时会给其代理函数发送一个一个方法
//切换前
optional public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
//切换后 1
optional public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)
//切换后2 发送
post UINavigationControllerDidShowViewControllerNotification
切换之前会想代理调用willShow回调,切换后会调用didShow,切换后也会发送UINavigationControllerDidShowViewControllerNotification 这个通知。如果我们把释放和加锁放入UINavigationController扩展里面,势必要会将UINavigationController.delegate变更成UINavigationController本身,或者添加这个UINavigationControllerDidShowViewControllerNotification这个通知,那我们必然面临两个问题
- 1.由于delegate被这个扩展使用,而其他真正使用代理做事情的类,无法再次使用代理
- 2.我们添加了这个通知, 那么什么时候释放它(ios 9以上,不需要释放通知),没有合适的地方。
所以我们形成了一个单例去在程序整个生命周期去管理这个事务,我们采用检测通知的方式。
单例中的跳转处理方法如下
func skipViewController(_ skipingController: UIViewController, skippedController: UIViewController?, skipType:UISkipControlSkipType, isAllowQueued:Bool, isAnimated:Bool, completionBlock:(()->Void)?) -> [AnyObject]? {
/// 合法性检测,VC对应的Window必须存在
weak var weakWindow = UIWindow.windowForViewController(skipingController)
if weakWindow == nil {
return nil
}
/// 构造切换完成后的清理工作
weak var weakSkippingController = skipingController
weak var weakSkippedController = skippedController
let freeCompetionBlock = {
//打印log
let strongSkippingController = weakSkippingController
let strongSkippedController = weakSkippedController
print("DID -- \(strongSkippingController) \(skipType.rawValue) \(strongSkippedController)")
//1. 切换完成后释放VC对应的Window的动画属性
let strongWindow = weakWindow
if (strongWindow != nil) {
strongWindow?.isAnimated = false
}
//将(1)和(2)加入到主线程队列中执行,主要目的在于让系统完成自己的清场任务后执行,否则有问题
// DispatchQueue.main.async {
//(1). 执行自定义的完成切换回调
if (completionBlock != nil) {
completionBlock!()
}
//(2). 执行该VC Window对应的队列
strongWindow?.performAnimationBlock()
//}
}
2.判断当前Window是否可以执行VC切换
if weakWindow != nil && weakWindow?.isAnimated == false {
//可以执行切换,先锁定window
weakWindow?.isAnimated = true;
//log
print("WILL -- \(skipingController) \(skipType.rawValue) \(skippedController)")
//执行切换
return self.performSkip(skipingController, skippedController: skippedController, skipType: skipType, isAnimated: isAnimated , completionBlock: freeCompetionBlock)
} else if (isAllowQueued){
//3.当前不能执行切换,但在允许加入队列的情况下,构造队列完成操作任务,加入到window队列
weak var weakSelf = self
weakWindow?.enqueueAnimationBlock {
let strongSelf = weakSelf
let strongSkippingController = weakSkippingController
//let strongSkippedController = weakSkippedController 取消对 skippedController weak持有,否则push popTo present 无法执行
if (strongSelf != nil && strongSkippingController != nil) {
//执行切换
strongSelf?.performSkip(strongSkippingController!, skippedController: skippedController, skipType: skipType, isAnimated: isAnimated, completionBlock: freeCompetionBlock)
}
}
//log
print("QUEUED -- \(skipingController) \(skipType.rawValue) \(skippedController)")
}
//log 当前无法进行切换
print("FAILED -- \(skipingController) \(skipType.rawValue) \(skippedController)")
return nil
}
代码稍微发杂了一点,原因是上面代码还考虑了另外一个需求,有时候我们冷启动Push,这个时候需要跳转,由于根本没有准备充足,直接跳转可能被阻挡,强制跳转可能会引起crash,所以我们增加了一个队列,捆绑在window上
fileprivate var skipAnimationQueue:[BlockObject]{
get {
if objc_getAssociatedObject(self, &skipAnimationQueueKey) != nil {
return objc_getAssociatedObject(self, &skipAnimationQueueKey) as! [BlockObject]
} else {
let queue:[BlockObject] = [BlockObject]()
self.skipAnimationQueue = queue;
return queue;
}
}
set {
objc_setAssociatedObject(self, &skipAnimationQueueKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
//队列执行函数
func performAnimationBlock()
{
if self.isAnimated == false {
if self.skipAnimationQueue.count > 0 {
let blockObject = self.skipAnimationQueue.first
if blockObject != nil {
blockObject!.performBlock()
self.skipAnimationQueue.removeFirst()
}
}
}
}
//加入队列
public func enqueueAnimationBlock(_ block:@escaping ()->()){
self.skipAnimationQueue.append(BlockObject(block: block))
}
在widow上增加了一个block数组作为队列。可以将某一次跳转加入到队列中,这样就能保证每次跳转都是有次序的。
现在看最上面的代码就不难理解,我们做了以下的事情
1.构建一个切换完成的 freeBlock ,来处理完成后的window释放和原本用户添加的完成块,最后队列,查看队列是否有切换需要执行,这个freeblock会绑定到UINavigationController上
2.判断当前window是否可以执行动画,如果可以就直接执行具体切换,
return self.performSkip(skipingController, skippedController: skippedController, skipType: skipType, isAnimated: isAnimated , completionBlock: freeCompetionBlock)
3.如果不能,查看调用是否有入队列的需求,有的话,加入UIWindwow的队列。
最后我们在接受通知的函数里面执freeblock
@objc public func handleNavigationControllerDidSkip(_ notification:NSNotification) {
var navigationtroller:UINavigationController?
if (notification.object != nil && notification.object is UINavigationController ) {
navigationtroller = notification.object as! UINavigationController?
if ((navigationtroller?.completionBlock) != nil) {
navigationtroller?.completionBlock!()
}
}
}
最后我们使用起来如下
let vc = UIViewController() self.navigationController?.pushViewController(vc, animated: true);
let vc1 = UIViewController()
self.navigationController?.pushViewController(vc, animated: true, allowQueued: true, completionBlock: nil) //成功,因为加入到队列了
普通的跳转和原来的系统的api一样不会有任何变化,如果需要加入对垒可以使用体用的新函数。
全部结束,关于Present,和push类似,文章最后又源码地址。
总结
我们队切换控制增加了三点
- 给UIWindow增加一个变量保证同一时间只有一个切换
- 增加一个单例来控制切换,解放出了UINavigationController的delegate的真正用途
- 增加了跳转队列,避免了有些业务跳转一定要保证完成,而不是window不能执行时丢弃该操作
swift github源码地址:UISkipControl
objective-c 源码地址 https://github.com/AKZHW/UISkipControl
coocoapods 引用 UISkipControl 版本 0.0.1