UIViewController Push & Pop 的那些坑

iOS開發中,UIViewController是最經常使用的一個類,在Push和Pop的過程當中也會常常出現一些UI卡死、App閃退的問題,本文總結了開發中遇到的一些坑。app

大部分視圖控制器切換致使的問題,根本緣由都是使用了動畫,由於執行動畫須要時間,在動畫未完成的時候又進行另外一個切換動畫,容易產生異常,假如在 Push 和 Pop 的過程不使用動畫,世界會清靜不少。因此本文只討論使用了動畫的視圖切換。也就是使用如下方式的 Push 和 Pop:ide

self.navigationController pushViewController:controller animated:YES];
[self.navigationController popViewControllerAnimated:YES];

1. 連續 Push

連續兩次 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

1.1 解決方案

繼承 UINavigationController 並重載 pushViewController 方法。spa

  1. 若是是動畫 Push,而且屬性 isSwitching == YES,則忽略此次 Push。
  2. 不然,設置 isSwitching = YES 再繼續切換。
  3. 等到動畫切換完畢,須要再把 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. 還原狀態
}

2. 連續 Pop

連續 Pop ,可能會致使兩種狀況。3d

2.1 self 被釋放

例如,下面的代碼,執行到第二句的時候,self 已經被釋放了。code

[self.navigationController popViewControllerAnimated:YES]; // self 被 release
[self.navigationController popViewControllerAnimated:YES]; // 繼續訪問 self 致使異常

2.2 界面異常、崩潰

假如你避開了上面那種調用,換成了這樣:繼承

[[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

從崩潰記錄徹底看不出緣由,十分坑爹。

2.3 解決方案

  • 方案一:第一次 Pop 不使用動畫。
  • 方案二:統一管理 Pop 的調用,若是當前正在 Pop,則下一次 Pop 先入棧;等到 Pop 執行完再執行下一次 Pop。

3. Push 的過程當中當即 Pop

Push 的過程當中調用 Pop,會致使界面卡死,表現爲:不響應任何點擊、手勢操做,可是不會崩潰。這也是在 iOS7 中出現的問題,iOS 8 以後不存在。

3.1 解決方案

同 1.1,重載 Pop 方法:

  1. Pop 的時候先判斷是否在切換中;
  2. 若是正在切換,則 Pop 的命令先保存到隊列;
  3. 切換動畫執行完畢,判斷是否須要處理 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];
        }
    }
}

4. Push 的過程當中手勢滑動返回

手勢滑動返回本質上調用的仍是 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];
        }
    }
}
相關文章
相關標籤/搜索