^_^嗨一下
文章目录
  1. 1. ViewController 的生命周期
    1. 1.1. ViewController 的初始化
      1. 1.1.1. Storyboard 方式的初始化
        1. 1.1.1.1. init(coder:)
        2. 1.1.1.2. awakeFromNib()
      2. 1.1.2. Code 方式的初始化
        1. 1.1.2.1. init(nibName:bundle:)
      3. 1.1.3. loadView()
      4. 1.1.4. viewDidLoad()
      5. 1.1.5. viewWillAppear(_:)
      6. 1.1.6. updateViewConstraints
      7. 1.1.7. viewWillLayoutSubviews()
      8. 1.1.8. viewDidLayoutSubviews()
      9. 1.1.9. viewDidAppear(_:)
        1. 1.1.9.1. 一个例子
      10. 1.1.10. viewWillDisappear(_:)
      11. 1.1.11. viewDidDisappear(_:)
      12. 1.1.12. didReceiveMemoryWarning()
      13. 1.1.13. deinit()
      14. 1.1.14. 几个例子
        1. 1.1.14.1. 屏幕旋转
        2. 1.1.14.2. Present & Dismiss
        3. 1.1.14.3. 死循环
        4. 1.1.14.4. push到下一个页面时
  2. 2. UIView 的生命周期
    1. 2.1. Xib & Nib
    2. 2.2. Code
    3. 2.3. 参考链接

ViewController 的生命周期

1
2
3
4
5
6
相信对于一个软件开发者来说,生命周期一定不是一个陌生的词语,我们软件开发所做的事情都是按照程序的生命周期进行的,在这个生命周期里边,我们和操作系统打着交道。在大部分的时间里,我们实际上并没有程序“控制权”,而是在配合系统或者一个类“正确”的完成他们的生命周期。以 ViewController 为例子,在 ViewController 中有一系列的生命周期方法,例如 loadView:、viewDidLoad:、viewWillAppear:、viewDidAppear、viewWillDisappear、viewDidDisappear 等等方法,这些方法似乎就像一个情报员,时刻告诉我们 ViewController 现在完成了什么、正在干什么、将要干什么。而我们只需要在正确的时间做正确的事情就行了,我们可以在某些方法里添加一些我们希望看到的视图、效果,而通过这些大量的定制组合就组成了我们的APP应用。
既然我们都是按照程序的生命周期在完成我们的工作,那么我们作为开发者来说,正确的掌握生命周期内的每个方法就显得尤为重要了,这不仅仅是为了代码看起来更加的优雅,也是为了正确的完成系统交给我们的任务。
一个类的生命周期,一定从 init... 开始,但实际开发中,ViewController 的入口几乎 99% 以上是 viewDidLoad ,这时,初始化已经完成,UIViewController 的属性 view 也已经被赋值,如果你重写了 loadView ,最后也会调用 viewDidLoad ,因此,这是个初始化子视图的好地方,这是因为:
1. viewDidLoad 只会被调用一次
2. 如果使用 Autolayout ,这时 子视图的 frame 属性还没有被设置,没有被设置,没有被设置 ,这几乎是你唯一设置绑定的时机(用编码方式设置自动布局),另一个时机是Storyboard中的手动绑定,这当然在viewDidLoad被调用之前。
最近刚好在 ViewController 的生命周期上面遇到了一些困扰,所以干脆研究下 ViewController 的生命周期

ViewController 的初始化

平常我们接触的 ViewController 初始化无非两种方式,一种是通过 Storyboard(Xib类似)初始化,而另外一种就是我们直接通过代码的方式初始化。当然,这两种方式初始化过程是有细微差别的,下面将分别介绍这两种方式的不同初始化过程

Storyboard 方式的初始化

Storyboard 在初始化阶段是由 init(coder:) -> awakeFromNib() 进行的。

init(coder:)

1
2
3
4
5
6
1. 当使用 Storyboard 时,控制器的构造器为 init(coder:)
2. 该构造器为必需构造器,如果重写其他构造器,则必须重写该构造器
3. 该构造器为可失败构造器,即有可能构造失败,返回 nil
4. 该方法来源自 NSCoding 协议,而 UIViewController 遵从这一协议
5. 该方法被调用意味着控制器有可能(并非一定)在未来会显示
6. 在控制器生命周期中,该方法只会被调用一次

awakeFromNib()

1
2
3
4
5
1.当使用 Storyboard 时,该方法会被调用
2.当调用该方法时,将保证所有的 outlet 和 action 连接已经完成
3.该方法内部必须调用父类该方法,虽然默认实现为空,但 UIKit 中许多类的该方法为非空
4.由于控制器中对象的初始化顺序不能确定,所以构造器中不应该向其他对象发送消息,而应当在 awakeFromNib() 中安全地发送
5.通常使用 awakeFromNib() 可以进行在设计时无法完成的必要额外设置

另外,由于是 Archive 并实例化对象,所以 View Controller 在初始化时调用的是 init(coder:)。手动调用 init 则不会从 nib 文件里加载,这个方法在执行 loadNibNamed: 一类的方法时就会被调用

Code 方式的初始化

不同 Storyboard 的是,Code 在初始化阶段是调用 init(nibName:bundle:) 这个方法

init(nibName:bundle:)

1
2
1.当使用纯代码创建控制器,控制器的构造器为 init(nibName:bundle:)
2.虽然使用代码创建时调用了该构造器,但传入的参数均为 nil

loadView()

View Controller 创建后需要加载 self.view 时会调用这个方法。此方法不应该被直接调用,如果我们的界面是在 Storyboard 中创建的,那我们也不应该覆盖这个方法

1
2
3
4
5
6
1.loadView() 即加载控制器管理的 view
2.不能直接手动调用该方法;当 view 被请求却为 nil 时,该方法加载并创建 view
3.若控制器有关联的 Nib 文件,该方法会从 Nib 文件中加载 view;如果没有,则创建空白 UIView 对象
4.如果使用 Interface Builder 创建 view,则务必不要重写该方法
5.可以使用该方法手动创建视图,且需要将根视图分配为 view;自定义实现不应该再调用父类的该方法
6.执行其他初始化操作,建议放在 viewDidLoad() 中

当 View Controller 有以下情况时都会在此方法中从 nib 文件加载 View :

  • View Controller 是从 storyboard 中实例化的
  • 通过 initWithNibName:bundle: 初始化
  • 在 App Bundle 中有一个 nib 文件名称和本类名相同

当我们调用 self.view 时,如果 self.view 非 nil,那么会直接调用该对象。如果为 nil,那么会调用 self.loadView() 创建一个 UIView 并将这个对象赋值给 self.view
在这个函数里调用 self.view 属性会造成死循环。因为访问 self.view 时发现该属性为空,会去调用 loadView() 方法,此时会造成死循环

viewDidLoad()

1
2
3
4
5
1.view 被加载到内存后调用 viewDidLoad()
2.重写该方法需要首先调用父类该方法
3.该方法中可以额外初始化控件,例如添加子控件,添加约束
4.该方法被调用意味着控制器有可能(并非一定)在未来会显示
5.在控制器生命周期中,该方法只会被调用一次

注意:此时 view 还没有被加入 view hierarchy 中,只是被加载入了内存中。在这里如果执行 self.presentViewController 之类的操作会出错

viewWillAppear(_:)

当 View 将要被添加到 View Hierarchy 中时会调用这个方法,每一次 View 将要显示时都会调用。在这个方法被调用时,也是在显示 View 所需要的动画被配置前

1
2
3
4
5
1.该方法在控制器 view 即将添加到视图层次时以及展示 view 时所有动画配置前被调用
2.重写该方法需要首先调用父类该方法
3.该方法中可以进行操作即将显示的 view,例如改变状态栏的取向,类型
4.该方法被调用意味着控制器将一定会显示
5.在控制器生命周期中,该方法可能会被多次调用

注意:这个时候在做一些和 frame 相关的操作时仍会出错,在这里 View 将要被加入 View Hierarchy,但是仍旧没有被添加进去

updateViewConstraints

updateViewConstraints 是 AutoLayout出现后新增的 api ,苹果官方的解释是该方法是用来添加约束的,重新该方法需要在方法实现的最后调用父类的该方法。并且这两个方法不建议直接调用
这里要注意,如果一个 view 或 controller 是由 interface builder 初始化的,那么这个实例的 updateViewConstraints 或 updateConstraints 方法便会被系统自动调用,起原因应该就是对应的 requiresConstraintBasedLayout 方法返回 true。而纯代码初始化的视图 requiresConstraintBasedLayout 方法默认返回 false。
所以在纯代码自定义一个 view 时,想把约束写在 updateConstraints 方法中,就一定要重写requiresConstraintBasedLayout 方法,返回true。

viewWillLayoutSubviews()

在 ViewController.view 将要布局 Subviews 时调用。当每一次界面的布局发生变化时都会被调用,例如旋转、被标记为需要 layout,在这之后 AutoLayout 会改变布局,在此方法中view的bound和orientation才最终确定,如果没有使用autoresizing mask或autolayout约束,可以在这个方法里布局subview,但是要注意,此方法只要根view的frame发生变化,或者标记为need layout,就会调用此方法,所以可能调多次,不适合做复杂的操作

1
2
3
4
5
6
1.该方法在通知控制器将要布局 view 的子控件时调用
2.每当视图的 bounds 改变,view 将调整其子控件位置
3.该方法可重写以在 view 布局子控件前做出改变
4.该方法的默认实现为空
5.该方法调用时,AutoLayout 未起作用
6.在控制器生命周期中,该方法可能会被多次调用

注意点:init初始化不会触发layoutSubviews
addSubview会触发layoutSubviews
设置view的Frame会触发layoutSubviews,当然前提 是frame的值设置前后发生了变化
滚动一个UIScrollView会触发layoutSubviews
旋转Screen会触发父UIView上的layoutSubviews事件
改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件

viewDidLayoutSubviews()

已经布局完成,也可以做一些操作,已通过 AutoLayout 布局

1
2
3
4
5
1.该方法在通知控制器已经布局 view 的子控件时调用
2.该方法可重写以在 view 布局子控件后做出改变
3.该方法的默认实现为空
4.该方法调用时,AutoLayout 已经完成
5.在控制器生命周期中,该方法可能会被多次调用

viewDidAppear(_:)

此时界面已经被显示出来了,做一些操作时可能会让界面变化可见

1
2
3
4
1.该方法在控制器 view 已经添加到视图层次时被调用
2.重写该方法需要首先调用父类该方法
3.该方法可重写以进行有关正在展示的视图操作
4.在控制器生命周期中,该方法可能会被多次调用

一个例子

通过 AutoLayout 在界面中添加一个 UITextView,在各个阶段输出它的 Frame 结果如下

  • viewDidLoad

    1
    2
    3
    frame = (20 40; 560 160);
    contentSize: {560, 133};
    contentOffset: {0, 0};
  • viewWillAppear

    1
    2
    3
    frame = (20 40; 560 160);
    contentOffset: {0, 0};
    contentSize: {560, 133};
  • viewWillLayoutSubviews

    1
    2
    3
    frame = (20 40; 560 160);
    contentOffset: {0, 0};
    contentSize: {560, 133};
  • viewDidLayoutSubviews

    1
    2
    3
    frame = (20 40; 374 296);
    contentOffset: {0, -15};
    contentSize: {374, 184}
  • viewDidAppear

    1
    2
    3
    frame = (20 40; 374 296);
    contentOffset: {0, -15};
    contentSize: {374, 184};

可以看到,从 viewDidLayoutSubview 开始, UITextView 的 Frame 发生了一次变化, contentSize 与 contentOffset 都发生了变化;因为在 viewWillAppear 及之前, view 还没有被加入层级中,布局还不是最终布局;所以在 viewWillAppear 及以前,计算 UITextView 行高等操作可能会出现问题;当呼出键盘后,viewWillLayoutSubview viewDidLayoutSubviews 将会被调用。可以看出,每一次布局发生变化时候这两个方法都会被调用

viewWillDisappear(_:)

1
2
3
1.该方法在控制器 view 将要从视图层次移除时被调用
2.类似 viewWillAppear(_:)
3.该方法可重写以提交变更,取消视图第一响应者状态

viewDidDisappear(_:)

1
2
3
1.该方法在控制器 view 已经从视图层次移除时被调用
2.类似 viewDidAppear(_:)
3.该方法可重写以清除或隐藏控件

didReceiveMemoryWarning()

1
2
3
1.当内存预警时,该方法被调用
2.不能直接手动调用该方法
3.该方法可重写以释放资源、内存

deinit()

1
1.控制器销毁时(离开堆),调用该方法

几个例子

屏幕旋转

1
2
3
4
5
6
7
8
willTransition(to:with:)
输出:
viewWillLayoutSubviews() - Optional((67.5, 269.5, 240.0, 128.0))
viewDidLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0))
viewWillLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0))
viewDidLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0))
viewWillLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0))
viewDidLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0))
  • 当 view 转变,会调用 willTransition(to:with:) 方法
  • 当屏幕旋转,view 的 bounds 改变,其内部的子控件也需要按照约束调整为新的位置,因此也调用了 viewWillLayoutSubviews()viewDidLayoutSubviews()

Present & Dismiss

1
2
3
4
5
6
输出:
viewWillDisappear
viewDidDisappear
viewDidDisappear
viewWillAppear
viewDidAppear
  • 当在一个控制器内 Present 新的控制器,原先的控制器并不会销毁,但会消失,因此调用了 viewWillDisappearviewDidDisappear 方法
  • 如果新的控制器 Dismiss,即清除自己,原先的控制器会再一次出现,因此调用了其中的 viewWillAppearviewDidAppear 方法

死循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LoopViewController: UIViewController {
override func loadView() {
print(#function)
}
override func viewDidLoad() {
print(#function)
let _ = view
}
}
输出:
loadView()
viewDidLoad()
loadView()
viewDidLoad()
loadView()
viewDidLoad()
loadView()
viewDidLoad()
loadView()
  • loadView() 没有加载 view,viewDidLoad() 会一直调用 loadView() 加载 view,因此构成了死循环,程序即卡死

push到下一个页面时

配合Storyboard方式的方法列表调用顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1. init(coder:)
2. awakeFromNib
3. willMoveToParentViewController:
4. prefersStatusBarHidden
5. preferredStatusBarUpdateAnimation
6. loadView
7. prepareForSegue:sender:
8. viewDidLoad
9. extendedLayoutIncludesOpaqueBars
10.edgesForExtendedLayout
11.viewWillAppear:
12.extendedLayoutIncludesOpaqueBars
13.edgesForExtendedLayout
14.updateViewConstraints
15.viewWillLayoutSubviews
16.viewDidLayoutSubviews
17.(Animation)
18.viewDidAppear:
19.didMoveToParentViewController:
20.updateViewConstraints
21.viewWillLayoutSubviews
22.viewDidLayoutSubviews

UIView 的生命周期

  • 通常,UIView 应尽可能避免重写构造器。
  • init(frame:):纯代码(指定构造器);init(coder:):Storyboard(必需可失败构造器)。若需要构造器,需要同时重写这两个构造器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func setup() {
// 如果本类有自定义变量,则此处不可初始化他们,
// 因为只有当初始化后才能调用自己的方法。
}
override init(frame: CGRect) {
super.init(frame: frame)
// 初始化变量
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// 初始化变量
setup()
}
  • awakeFromNib() 只在使用 Storyboard 的 UIView 中被调用。
  • awakeFromNib() 并不是构造器,但它在初始化完成后立即被调用。
  • 所有 Storyboard 中继承自 NSObject 的对象发送该消息。但顺序是不确定的,因此不能在这里调用其他任何 Storyboard 中的对象

Xib & Nib

  • ib 是 Interface Builder 的缩写,即界面构造器。这里简要说下,Xib 和 Nib 各是什么,有什么区别
  • Xib 实际是一个 XML 文件,而 Nib 是二进制文件。当应用编译时,Xib 文件被翻译为 Nib。所以在 Xcode 中,我们可以自己新建 Xib 文件来构造 UI,而当编译时,Xcode 会自动生成相应的 Nib 文件,而不需我们额外关注。关于其详细介绍,您可以参考文末的资料
    我们现在新建一个继承UIView的MyView类,并勾选Xib,并将 Utilities 中 Identity inspector 的 Custom Class 改为 MyView
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import UIKit
    class MyView: UIView {
    override init(frame: CGRect) {
    super.init(frame: frame)
    print("init(frame:)")
    }
    required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    print("init(coder:)")
    // fatalError("init(coder:) has not been implemented")
    }
    override func awakeFromNib() {
    super.awakeFromNib()
    print("awakeFromNib()")
    }
    }

之后运行即可在屏幕上看到该自定义 UIView,控制台输出:

1
2
3
输出
init(coder:)
awakeFromNib()

通过打印的输出,可以看出使用 Interface Builder 载入 View 不会调用 init(frame:) 方法,而是调用了 init(coder:)init(coder:) 是 NSCoding 协议中的方法,NSCoding 是负责编码解码,归档处理的协议

1
required init?(coder aDecoder: NSCoder)

init(coder:) 的调用处于 Nib 载入时,而 awakeFromNib() 的调用处于 Nib 载入后。Nib 的载入过程如下:

  • Nib 文件内容和引用的资源文件加载到内存;
  • 反归档存储于 Nib 文件的图像数据对象并初始化;
  • 遵从 NSCoding 的对象(UIView & UIViewController)调用 init(coder:)
  • 其他对象调用其他构造器方法
  • 建立对象间连接:Outlet & Action
  • 实现 awakeFromNib() 的对象调用该方法

需要注意的是,awakeFromNib() 中需要调用父类的该方法以保证父类的进行额外初始化。而在本例中重写的 init(coder:) 目的主要是查看调用顺序,并没有加入特别的操作。因此在实际使用中,如果使用 Interface Builder,可以不重写该方法

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import UIKit
class MyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
print("init(frame:)")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
print("awakeFromNib")
}
}

之后在其他UIView或者UIController调用该类的时候

1
2
输出
init(frame:)

通过纯代码创建自定义 UIView,便只调用 init(frame:) 方法,不涉及 Nib 的方法,因此不会调用 awakeFromNib()init(coder:) 方法。而由于 init(coder:) 为必要构造器,因此重写 init(frame:) 时,必须实现该方法

有时,为了便于从 Interface Builder 和纯代码都能创建自定义 UIView 对象,可以将 init(coder:) 方法改为:

1
2
3
4
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// fatalError("init(coder:) has not been implemented")
}

若保留 fatalError(),则从 Nib 初始化时会无条件输出语句并停止运行

参考链接

https://juejin.im/entry/57988af4d342d300590d5445
https://juejin.im/post/58c20a761b69e6006bc965ee
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html#//apple_ref/doc/uid/TP40014097-CH18-ID203
https://github.com/kingcos/CS193P_2017
http://amztion.com/2016/12/03/uiviewcontroller-lifecycle/
http://www.infoq.com/cn/articles/ios-app-arch-2-1

文章目录
  1. 1. ViewController 的生命周期
    1. 1.1. ViewController 的初始化
      1. 1.1.1. Storyboard 方式的初始化
        1. 1.1.1.1. init(coder:)
        2. 1.1.1.2. awakeFromNib()
      2. 1.1.2. Code 方式的初始化
        1. 1.1.2.1. init(nibName:bundle:)
      3. 1.1.3. loadView()
      4. 1.1.4. viewDidLoad()
      5. 1.1.5. viewWillAppear(_:)
      6. 1.1.6. updateViewConstraints
      7. 1.1.7. viewWillLayoutSubviews()
      8. 1.1.8. viewDidLayoutSubviews()
      9. 1.1.9. viewDidAppear(_:)
        1. 1.1.9.1. 一个例子
      10. 1.1.10. viewWillDisappear(_:)
      11. 1.1.11. viewDidDisappear(_:)
      12. 1.1.12. didReceiveMemoryWarning()
      13. 1.1.13. deinit()
      14. 1.1.14. 几个例子
        1. 1.1.14.1. 屏幕旋转
        2. 1.1.14.2. Present & Dismiss
        3. 1.1.14.3. 死循环
        4. 1.1.14.4. push到下一个页面时
  2. 2. UIView 的生命周期
    1. 2.1. Xib & Nib
    2. 2.2. Code
    3. 2.3. 参考链接