不知道你们有没有考虑过一个很奇怪的状况,就是 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
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
override func loadView() {
print(self.view)
super.loadView()
}
复制代码
能够看出,[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
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadView()
}
复制代码
表面上看起来没有任何变化,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 被换掉以后,以后的生命周期没有被触发,因此也不会打印以后的生命周期。
override func viewDidLoad() {
super.viewDidLoad()
loadView()
}
复制代码
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 被加入视图层级以前将其替换掉,并不影响它被加入视图层级之中,因而也就能够正常跳转了。
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()
复制代码
小问题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 生命周期的缺失和错乱)