前言
-
本文从生命周期的角度入手,深入探究:
- 在loadView、viewDidLoad、viewWillAppear的过程中,viewController的view发生了怎样的变化
- loadView、awakeFromNib、initWithNibName这些不常使用的方法/回调,在 viewController的生命周期中究竟做了什么工作
- viewController是否绑定了xib,对viewController.view的构建带来的影响
- 当你从viewControllerA push到 viewControllerB时
- viewControllerB的生命周期是如何开始的
- viewControllerA的生命周期是如何结束的
-
阅读本文,你应该:
- 已经熟练使用ViewController,但不明白ViewController的实现、运作过程
- 有深入了解ViewController生命周期的兴趣,然而百度上良莠不齐的资料已经无法满足你
- 有一定英文功底,有啃得下官方文档的耐心
本文全篇翻译自官方文档,润色过程中难免有少许疏漏错误,若有高人指点,感激不胜
先上结论
-
出生:一个ViewController有多种实例化方法,大体分三种:
- 代码中通过init实例化
- 代码中通过initWithNibName实例化
- storyboard间接实例化(即调用initWithCoder实例化)。
如果是通过storyboard间接实例化的,则会紧接着会调用awakeFromNib- 这个方法本身只是个信号、消息,是一个空方法(即其默认实现为空)
- 标志着ViewController从Nib脱壳而生,呱呱坠地
-
为View的加载做准备:
- loadView
- 顾名思义,这个方法中,要正式加载View了
- 在ViewController引用的View第一次被第三者引用时,编译器会调用这个方法
- 在这个方法刚开始,ViewController所引用的View都是nil
- 在这个方法一结束,ViewController所引用的View都已经实例化
- 如果这个ViewController与nib有关(包括storyboard),则编译器解压nib得到ViewController的View
- 如果这个ViewController与nib无关(包括storyboard),你可以在这里手写ViewController的View
(这一步大概也可以在ViewDidLoad里写,实际上我们也更常在ViewDidLoad里写)
- viewDidLoad
- 在这一步之前创造好的视图层(View hierachy),只是实例化了,但是没有“被addSubView”到ViewController的根view
- 这个方法大概也是一个空方法(因为不强制要求调用super方法),只是个信号、消息
- 标志着编译器提醒ViewController
- View已经加载好了(即“didLoad”)
- 下一步ViewController的根View将要链接到之前创造好的视图层(View hierachy)
- loadView
-
view正式出场
- viewWillAppear/viewDidAppear
- viewWillDisappear/viewDidDisappear
接下来是怎么探究出这个结论、以及更多相关细节的正文
周期总览
-
当一个ViewController被创建,并在屏幕上显示的时候
- alloc
- init / initWithNibName / initWithCoder
- loadView
- viewDidLoad
- viewWillAppear/viewDidAppear
-
当一个视图被移除、销毁的时候的执行顺序
- viewWillDisappear/viewDidDisappear
- dealloc
具体解释
init
初始化对象,初始化数据
注意:
- 调用[super init]一定会调用initWithNibName,无论是否有xib文件
- 如果通过Storyboard跳转来间接初始化ViewController,则只会调用initWithCoder,不会调用initWithNibName和init
- 可见稍后实测结果
initWithNibName
全名
- (instancetype)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle
作用
得到一个通过给定nib文件新建的ViewController-
参数
- nibName:xib的名字(不带路径)
- bundle:搜索nib文件的bundle名字(需要带路径)。当为nil时,该方法通过heuristics(启发法?)自动搜索该nib
This is the designated initializer for this class.
这是viewController的init系列方法之一When instantiating a view controller from a storyboard, iOS initializes the new view controller by calling its initWithCoder: method instead of this method
(非常关键)当你从storyboard初始化viewController时,iOS会使用initWithCoder,而不是initWithNibName来初始化这个viewControllerand sets the nibName property to a nib file stored inside the storyboard.
(非常关键)然后那个storyboard会在自己内部生成一个nib
(storyboard实例化view/viewController时,会把nib的信息放在Coder中,调用initWithCoder)
loadView
- You should never call this method directly.
- The view controller calls this method when its view property is requested but is currently nil.
如果有一个viewController,它的view被访问了而且为空,则它就会调用loadView方法 - This method loads or creates a view and assigns it to the view property.
这个方法会加载或创建(被请求的)一个view,并分配给viewController自己(即self.view) - If the view controller has an associated nib file, this method loads the view from the nib file.
如果viewController有一个绑定的nib file,这个方法会从nib中加载view - A view controller has an associated nib file if the nibName property returns a non-nil value,which occurs
如果viewController的@property——nibName的getter返回了一个非空的值,说明它有一个绑定的nib file。
这可以分三种情况讨论:- if the view controller was instantiated from a storyboard
该ViewController由storyboard实例化 - if you explicitly assigned it a nib file using the initWithNibName:bundle: method
明确地通过initWithNibName:bundle:方法初始化这个viewController - if iOS finds a nib file in the app bundle with a name based on the view controller's class name
编译器发现在app bundle中有一个nib与这个ViewController的名字正好匹配
- if the view controller was instantiated from a storyboard
- If the view controller does not have an associated nib file, this method creates a plain UIView object instead.
如果ViewController并没有绑定nib文件,这个方法则赋予ViewController一个空的UIVIew - If you use Interface Builder to create your views and initialize the view controller, you must not override this method.
如果你用了Interface Builder,通常这一步不需要去干涉。
若你没有使用xib文件创建视图,你可以这么写来手动创建视图:
- (void)loadView {
//注意,这里没有调用super方法
CGFrame frame = [[UIScreen mainScreen] applicationFrame];
UIView *myview = [[UIView alloc] initWithFrame: frame];
self.view = myview;
//该view不应该被除此之外的对象引用
}
viewDidLoad
- Notifies the view controller that its view is about to be added to a view hierarchy.
(大概是大家最熟的...) - This method is called after the view controller has loaded its view hierarchy into memory.
在ViewController在内存中加载完了view层级后,该方法被调用 - This method is called regardless of whether the view hierarchy was loaded from a nib file or created programmatically in the loadView method.
无论loadView是咋样的,该方法一定会在loadView之后被调用 - You usually override this method to perform additional initialization on views that were loaded from nib files.
通常重写该方法,来为从nib文件中加载完毕的view做一些额外的构造
此时viewController的property outlet view已经不是nil了,但仍然没有显示在屏幕中
我曾在viewDidLoad加入第三方动画等待框、或是present/push,程序会崩溃,原因就是此时view只是被load了,仍然在内存中,还没有appear
viewWillAppear
- This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view.
在viewController的self.view要被链接到视觉层之前、在动画可以开始运作之前,这个方法会被调用 - You can override this method to perform custom tasks associated with displaying the view.
- For example, you might use this method to change the orientation or style of the status bar to coordinate with the orientation or style of the view being presented.
比如,你可以重写这个方法,使status bar和将要展现的view双方的orientation、 style协调 - If you override this method, you must call super at some point in your implementation
viewDidAppear
此时ViewController的视图已经展现在屏幕中了
- Notifies the view controller that its view was added to a view hierarchy.
- You can override this method to perform additional tasks associated with presenting the view.
- If you override this method, you must call super at some point in your implementation.
其他相关方法
awakeFromNib
简述
Prepares the receiver for service after it has been loaded from an Interface Builder archive, or nib file.
做一个接受准备,在对象已经被一个Interface Builder archive或者nib file加载之后
晚上很多博客对这个方法的理解都不甚正确、以讹传讹,或是浅尝辄止,所以我直接照搬了官方文档,并加点自己的粗陋的翻译,以作探究
- 补充:这个方法很让人恼火,虽然名字里有FromNib,但是其实是一个界定模糊的概念
- 当storyboard调用某个ViewController,会走awakeFromNib
- 当建立自定义ViewController(+xib)时,不会走awakeFromNib
- 当建立自定义View(+xib)的时候才会走- (void)awakeFromNib方法。
以上结论似乎与(+xib)无关(待复查)
状态境况
- Note:You must call the super implementation of awakeFromNib to give parent classes the opportunity to perform any additional initialization they require. Although the default implementation of this method does nothing, many UIKit classes provide non-empty implementations. You may call the super implementation at any point during your own awakeFromNib method.
这一点很像java中的abstract method
这段很长但是很易懂,其中有一句非常重要的话:Although the default implementation of this method does nothing该方法默认实现为空。
后面我会基于这句话做解释。 - The nib-loading infrastructure sends an awakeFromNib message to each object recreated from a nib archive, but only after all the objects in the archive have been loaded and initialized.
nib在加载时,会传递awakeFromNib消息给与它相关的所有对象,在那个时刻,这些对象全部都已经加载、初始化完毕 - When an object receives an awakeFromNib message, it is guaranteed to have all its outlet and action connections already established.
当某对象收到了awakeFromNib的消息,编译器能保证,Outlets一定都插好了(@property与action)
方法作用
- During the instantiation process, each object in the archive is unarchived and then initialized with the method befitting its type
在这个实例化的过程中,在nib的archive的所有对象会被解压缩,然后会调用合适的方法初始化 - Objects that conform to the NSCoding protocol (including all subclasses of UIView and UIViewController) are initialized using their initWithCoder: method. All objects that do not conform to the NSCoding protocol are initialized using their init method.
实现了NSCoding协议的对象会调用initWithCoder:方法初始化,其余对象会调用init方法初始化 - After all objects have been instantiated and initialized, the nib-loading code reestablishes the outlet and action connections for all of those objects. It then calls the awakeFromNib method of the objects.
当所有对象都被实例化、初始化后,nib加载的代码会恢复与这些对象的outlet和action。之后,这些对象会调用awakeFromNib方法。(实质是一个递归调用)
理解
怎么理解这段话呢?
我做了一个实验,发现在awakeFromNib结束之前,viewController类的(outlet property)button为nil
而awakeFromNib执行完之后,viewDidLoad之前,button已经初始化完毕了:
- (注:以下的nib包括前述的storyboard內建nib)如上所述,awakeFromNib,只一个默认实现为空的方法,该方法只表明了一个时间点,即nib文件开始解压缩之前。
- 这个时间点是纯粹让你做一些特殊的准备用的(如下段),你如果不做修改,它就是一个空方法。
- 而在这个时间点,viewController的outlet property view一定是nil。这意味着你即使要重写awakeFromNib,也一定不是针对这些outlet property view的
- 在awakeFromNib之后,编译器会调用(至少文档上没写的)loadView,给nib的archive做解压缩(如果这个archive含有子nib的话,则为继续递归调用)、初始化
- 当nib中的全部元素都初始化完毕后,(大概)viewDidLoad就来了,后面就是熟悉的情节了
运用举例
- Typically, you implement awakeFromNib for objects that require additional set up that cannot be done at design time.
一般来说,有些对象在storyboard搭建中,没有完全做好准备,而你可以为这些对象实现awakeFromNib来继续做准备 - For example, you might use this method to customize the default configuration of any controls to match user preferences or the values in other controls.
但似乎是说,你可以使用这个方法,来自由配置你想要的设置,以符合用户偏好,或者其他情况的需要 - You might also use it to restore individual controls to some previous state of your application.
你可以使用这个方法,来恢复app回到之前状态的配置 - 注意这个过程,没有调用super方法。
viewWillLayoutSubviews
Called to notify the view controller that its view is about to layout its subviews.
- 这里的layout应理解为:使...布局。即给subviews重新布局
- The default implementation of this method does nothing.
- did版本:viewDidLayoutSubviews
viewDidUnload
- Deprecated:In iOS 6 and later, clearing references to views and other objects in your view controller is unnecessary.
- 即现在已经不需要这个方法了
分析一个情境:vc1 push到 vc2的生命周期流程
- 代码测试
- (IBAction)click2:(id)sender {
ViewController *vc = [[ViewController alloc]init];
[self.navigationController pushViewController:vc animated:YES];
}
- 测试结果
- 调用
[[ViewController alloc]init];
时- init
- (
[super init]
中调用了)initWithNibName
[self.navigationController pushViewController:vc animated:YES];
- 然后代码会继续执行,直到回到RunLoop()没研究过RunLoop,只是做个猜测)
- 然后是这样的
- 调用
2016-12-06 21:31:21.707 test[53020:770888] viewWillDisappear
2016-12-06 21:31:21.707 test[53020:770888] loadView
2016-12-06 21:31:21.708 test[53020:770888] viewWillAppear
2016-12-06 21:31:22.263 test[53020:770888] viewDidDisappear
2016-12-06 21:31:22.263 test[53020:770888] viewDidAppear
代码实践
- 我观察了init、initWithCoder、initWithNibName、awakeFromNib的调用顺序,通过三种不同的ViewController实例化方式:storyboard间接初始化、init初始化、initWithNibName初始化
代码:
- (void)awakeFromNib {
[super awakeFromNib];
NSLog(@"awakeFromNib");
}
- (instancetype)init {
self = [super init];
NSLog(@"init");
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
NSLog(@"initWithCoder");
return self;
}
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
NSLog(@"initWithNibName");
return self;
}
测试结果:
storyboard间接初始化
2016-12-06 19:41:01.523 test[47338:660263] initWithCoder
2016-12-06 19:41:01.523 test[47338:660263] awakeFromNib
init初始化(不带xib)
2016-12-06 19:41:17.583 test[47338:660263] initWithNibName
2016-12-06 19:41:17.584 test[47338:660263] init
init初始化(带xib)
2016-12-06 19:41:41.991 test[47338:660263] initWithNibName
2016-12-06 19:41:41.992 test[47338:660263] init
initWithNib初始化
2016-12-06 19:41:54.528 test[47338:660263] initWithNibName
- 我观察到的一些结果:
- storyboard间接初始化ViewController中,确实没有调用initWithNibNamed方法
- 通过initWithNibName初始化ViewController中,确实没有调用awakeFromNib方法
- 只有storyboard间接初始化ViewController中会调用awakeFromNib方法
- 调用[super int]时一定会调用initWithNibName方法,即使不存在xib文件
其他收获
- Objective-C的重写方法中,有些是需要调用super方法的,有些则不是。
有些方法,你不调用super方法,会warning,如awakeFromNib
有些方法,无需调用super方法,甚至文档都写得很清楚:“The implemention of method is nil”
有趣的是viewDidLoad是无需super的,而viewWillAppear是必须super的 - 没有源码
iOS开发看不到源码的实现,绝大部分信息只能从文档获取
然而...这就体现了英文水平的重要性了,读点英文文档真是太累了