iOS 生命週期的缺失和錯亂

不知道你們有沒有考慮過一個很奇怪的狀況,就是 View Controller 的生命週期沒有被調用,或者是調用順序錯亂?其實這在實際操做中常常發生,override 的時候一不當心就忘記調用 super 了,或者明明是 override viewWillAppear(),卻調用成了 super.viewWillDisappear()。甚至,一不當心,調用了兩次…git

override func viewWillAppear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    // 寫成這樣會被罵死嗎 =。=
}
複製代碼

那麼這究竟會發生致使什麼問題呢?github

咱們先簡單寫一個 demo 方便咱們提問(demo 地址:LifeCycleDemo,很是簡單,本身寫一個也行)。就是一個用 Storyboard 新建了一個 ViewController,而後能夠跳轉到另外一個 ViewController。swift

而後,咱們在 ViewController 的每個生命週期被調用時都打印一下生命週期的名字,就是下面這樣:bash

好,有了這個 Demo 以後,咱們依照這個 Demo 來討論下面幾個問題:app

1. 若是缺乏 loadView() 方法會怎麼樣?

override func loadView() {
    // super.loadView()
    print("loadView")
}
複製代碼

答案:黑屏

這道題很假單,若是沒有 loadView,那就沒有加載 view,就是黑屏。ide

Apple 文檔中說,loadView 不能被手動調用,View Controller 會自動在其 View 第一次被獲取、而且仍是 nil 的時候調用(能夠理解爲 View 是懶加載的)。若是你要 override 這個方法,那麼必需要將你本身的 view hierarchy 中的 root View 設置給 View Controller 的 View 屬性。而且這個 View 不能與其餘 View Controller 共享,也不能再調用 super 方法了。post

2. 若是在 loadView() 以前調用 view 會怎麼樣?

override func loadView() {
    print(self.view)
    super.loadView()
}
複製代碼

答案:infinite stack trace

能夠看出,[UIViewController view] 和 ViewController.loadView 循環調用了。這是由於在 loadView 以前,view 並無被建立,而因爲 view 是懶加載的,此時調用 self.view 會觸發 loadView,由此致使了循環引用。ui

另外,若是咱們想要重寫 loadView,正確的方式應該相似於這樣:spa

override func loadView() {
    let myView = MyView()
    view = myView
}
複製代碼

實際上,重寫 loadView 能達到一些意想不到的效果,推薦一篇文章:重寫 loadView() 方法使 Swift 視圖代碼更加簡潔code

3. 若是在 viewWillAppear() 時候手動調用 loadView() 會怎麼樣?

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    loadView()
}
複製代碼

答案:ViewController 的 view 被替換

表面上看起來沒有任何變化,ViewController 仍是能完整地顯示出來。可是這個時候若是咱們點擊 "Presented Controller" 這個按鈕,想要跳轉到下一個頁面,會發現沒有響應。同時會發現 Console 中有下面的輸出:

Warning: Attempt to present <LifeCycleDemo.PresentedViewController: 0x7fe4f601def0> on <LifeCycleDemo.ViewController: 0x7fe4f6212e50> whose view is not in the window hierarchy!
複製代碼

很明顯的,因爲咱們在手動調用了 loadView 方法,致使 ViewController 中原本的 view 新建了兩次。新的 view 替換了原來的 view,致使新 view 的視圖層級出錯了,因而在進行 present 操做的時候就發生了上述錯誤。

爲了驗證一下,咱們能夠在調用 loadView() 以前和以後分別 print(self.view!),會發現 ViewController 的 view 確實被替換掉了,結果以下:

loadView
viewDidLoad
<UIView: 0x7fef58c089d0; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x60000272b280>>
loadView
<UIView: 0x7fef58c1c220; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x60000272ba80>>
viewWillAppear
複製代碼

同時咱們發現一個有趣的現象,以後的生命週期沒有被打印出來了(並非我沒有複製粘貼上來!)。能夠合理推斷 viewDidAppear 等實際上監聽的仍是第一個 view 的變化,而因爲第一個 view 被換掉以後,以後的生命週期沒有被觸發,因此也不會打印以後的生命週期。

4. 若是在 viewDidLoad() 時候手動調用 loadView() 會怎麼樣?

override func viewDidLoad() {
    super.viewDidLoad()
    loadView()
}
複製代碼

答案:view 被替換可是能夠正常跳轉

loadView
<UIView: 0x7ff917519350; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x600000e8bd80>>
loadView
<UIView: 0x7ff91a407a50; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x600000ef1120>>
viewDidLoad
viewWillAppear
viewSafeAreaInsetsDidChange
viewWillLayoutSubviews
viewDidLayoutSubviews
viewDidAppear
複製代碼

咱們輸出生命週期以後,發現手動調用 loadView 以後 view 確實被替換了。可是爲何這一次,以後的生命週期就被正常打印出來了,而且再跳轉的時候也能夠正常跳轉呢?

能夠推測,底層在將 view 加入到視圖層級,而且開始監聽 viewWillAppear 等生命週期的時機,是在 viewDidLoad 以後,viewWillAppear 以前的。因此若是在 view 被加入視圖層級以前將其替換掉,並不影響它被加入視圖層級之中,因而也就能夠正常跳轉了。

5. 如錯誤調用 viewWillAppear 等方法會怎麼樣?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidDisappear(animated) // 調用錯了!
}

override func viewDidDisappear(_ animated: Bool) {
    // super.viewDidDisappear(animated) 忘記調用了
}
複製代碼

答案:繼承時可能有問題

根據代碼註釋描述能夠知道,實際上這些方法並無實際上作什麼事情,只是在特定的時間節點,起到一個通知的做用。因此在咱們的 demo 裏,錯誤調用、不調用不會有什麼實質上的錯誤。可是因爲咱們在複雜的項目中會有很是複雜的繼承關係,若是中間有一個地方錯了,那麼極可能影響繼承關係中的其餘 ViewController。因此仍是應該嚴格準確地調用 super 方法。

那麼,如何來保證正確地調用 super 方法呢?在 Objective-C 中,可使用 __attribute__((objc_requires_super)); 或者 NS_REQUIRES_SUPER 屬性(實際功效都是相同的),好比新建一個 BaseViewController 做爲全部類的基類,而後這樣寫:

// Objective-C 保證調用 super 方法
@interface BaseViewController : UIViewController

- (void)viewDidLoad __attribute__((objc_requires_super));

- (void)viewWillAppear:(BOOL)animated NS_REQUIRES_SUPER;

@end
複製代碼

(參考答案:Stack Overflow - nhgrif's answer)

若是是 swift 呢?目前 swift 沒有上面這種代碼層面的解決辦法,只能藉助 SwiftLint 進行靜態檢查。按照官方文檔引入 SwiftLint 後,在 yml 文件中加入下面的描述便可強制檢查,override 的時候是否調用響應方法的 super(這也能夠用於檢查自定義的 class):

// Swift 保證調用 super 方法
overridden_super_call:
  severity: error
  included:
    - "*"
    - viewDidLoad()
    - viewWillAppear()
    - viewDidAppear()
    - viewWillDisappear()
    - viewDidDisappear()
複製代碼

6. 最後兩個小問題

小問題1:在當前屏幕上加一個全屏的 window,會觸發下面的 ViewController 的 viewWillAppear 等方法嗎?

答案:不會,這些方法只關注在同一個 view hierarchy 下的變化。同理,鎖屏後進入,後臺進前臺等都不會觸發。

小問題2:如何斷定一個 ViewController 是否可見?

答案Stack Overflow - progrmr's answer

可使用 view.window 方法來判斷,可是須要注意加上 isViewLoaded,來防止在 ViewController 的 view 沒有被初始化過的時候被調用,而觸發它的懶加載。

if (viewController.isViewLoaded && viewController.view.window) {
    // viewController is visible
}
複製代碼

另外,在 iOS 9+,也可使用下面這個更加簡潔的方式:

if viewController.viewIfLoaded?.window != nil {
    // viewController is visible
}
複製代碼

(本文 Github 連接:RickeyBoy - iOS 生命週期的缺失和錯亂

相關文章
相關標籤/搜索