经常会用到 ViewController,但是对它的生命周期一直没有一个比较完整地理解,最近看了几篇博客,在这里对 ViewConroller 的生命周期做一个总结,一是为了自己学习,二是为了给大家一个参考,如有错误,欢迎指正。
ViewController 总体生命周期:
1.ViewController 多种实例化方法
1.1 代码
通过 xib 加载
先看一下 Demo 的文件结构,ViewController 为 A 控制器,TestViewController 为 B 控制器。
当控制器 view 通过 xib 加载的时候,可能会出现三种情况:
指定xib名称(OtherViewController.xib)
TestViewController *testVC = [[TestViewController alloc] initWithNibName:@"OtherViewController" bundle:nil];复制代码
当我们指定了xib的名称,loadView方 法就会去加载对应的 xib (OtherViewController.xib),最终是这个样子的。
不指定 xib 名称1
TestViewController *testVC = [[TestViewController alloc] initWithNibName:nil bundle:nil];复制代码
如果我们不指定 xib 名称,loadView 就会加载与控制器同名的 xib (TestViewController.xib),最终是这个样子的。
不指定 xib 名称2
我们先将 TestViewController.xib 这个文件删除掉,这个时候,我们再来运行程序,结果是这样的。
根据上图我们可以得知,当没有指定 xib 名称,且没有与控制器同名的 xib 时,会加载前缀与控制器名相同而不带 Controller 的 xib (TestView.xib)。
init
我们经常会用代码通过 init 手动创建一个 ViewController,如下:
TestViewController *testVC = [[TestViewController alloc] init];复制代码
其实本质还是调用了 initWithNibName:bundle: 并且都传入了 nil,只不过以上三种情况都没有满足,最终是这个样子的。
1.2 storyboard 间接实例化(initWithCoder)
当你从 storyboard 初始化 ViewController 时,iOS 会使用 initWithCoder,而不是 initWithNibName 来初始化这个 ViewController,然后那个 storyboard 会在自己内部生成一个 nib (storyboard 实例化 view / ViewController 时,会把 nib 的信息放在 Coder 中,调用 initWithCoder)。
注意
storyboard 加载的是控制器及控制器 view,而 xib 加载的仅仅只是控制器的 view。之所以这么说,我们结合控制器的 awakeFromNib 方法解释一下,顾名思义,当控制器从 nib 加载的时候就会调用这个方法,这个方法本身只是个信号、消息,是一个空方法 (即其默认实现为空)。
先来看看通过 storyboard 加载的情况:
//A控制器中代码- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"TestViewController" bundle:nil];TestViewController *testVC = [storyboard instantiateInitialViewController];[self.navigationController pushViewController:testVC animated:YES];}//B控制器中代码- (void)awakeFromNib {NSLog(@"B通过nib加载");}复制代码
调用了 B 控制器的 awakeFromNib 方法。
将之前删除的 TestViewController.xib 文件重写添加进去,再来看通过xib加载的情况:
//A控制器中代码改为如下- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{TestViewController *testVC =[[TestViewController alloc] init];[self.navigationController pushViewController:testVC animated:YES];}//B控制器中代码不变复制代码
B 控制器的 awakeFromNib 方法并没有被调用。
所以,storyboard 加载的是控制器及控制器 view,而 xib 加载的仅仅只是控制器的 view。
2. loadView
这个方法中,要正式加载View了。首先我们得知道,控制器 view 是通过懒加载的方式进行加载的,即用到的时候再加载。永远不要主动调用这个方法。当我们用到控制器 view 时,就会调用控制器 view 的 get 方法,在 get 方法内部,首先判断 view 是否已经创建,如果已存在,则直接返回存在的 view,如果不存在,则调用控制器的 loadView 方法,在控制器没有被销毁的情况下,loadView 也可能会被执行多次。
当 ViewController 有以下情况时都会在此方法中从 nib 文件加载 view :
ViewController 是从 storyboard 中实例化的。
通过 initWithNibName:bundle: 初始化。
在 App Bundle 中有一个 nib 文件名称和本类名相同。
符合以上三点时,也就不需要重写这个方法,否则你无法得到你想要的 nib 中的 view。
如果这个 ViewController 与 nib 无关,你可以在这里手写 ViewController 的 view (这一步大概也可以在 viewDidLoad 里写,实际上我们也更常在 viewDidLoad 里写)。
是否需要调用 [super loadView]
loadView 方法的默认实现是这样:先寻找有关可用的 nib 文件的信息,根据这个信息来加载 nib 文件,如果没有有关 nib 文件的信息,默认实现会创建一个空白的 UIView 对象,然后让这个对象成为 controller 的主 view。
所以,重写这个函数时,你也应该这么做。并把子类的 view 赋给 view 属性 (property) (你 create 的 view 必须是唯一的实例,并且不被其他任何 controller 共享)。
如果你要进行进一步初始化你的 views,你应该在 viewDidLoad 函数中去做。在iOS 3.0 以及更高版本中,你应该重载 viewDidUnload 函数来释放任何对 view 的引用或者它里面的内容(子 view 等等)。
回到关于 [super loadView] 的讨论中,如果我们的 ViewController 与 nib 有关,也就是说我们不需要重写 loadView 方法,也就不用关心 [super loadView]。而如果与 nib 无关,我们需要重写 loadView 方法,而 [super loadView] 根据上面的解释就会生成一个空白的 view,这恐怕并不能满足我们的需求,所以调用也没有多大意义。
2.1 ViewController 加载 View 过程
从图中可以看到,在 view 加载过程中首先会调用 loadView 方法,在这个方法中主要完成一些关键 view 的初始化工作,比如 UINavigationViewController 和 UITabBarController 等容器类的 ViewController;接下来就是加载 view,加载成功后,会接着调用 viewDidLoad 方法,这里要记住的一点是,在 loadView 之前,是没有 view 的,也就是说,在这之前,view 还没有被初始化。完成 viewDidLoad 方法后,ViewController 里面就成功的加载 view了,如上图右下角所示。
死循环
若 loadView 没有加载 view,即为 nil,viewDidLoad 会一直调用 loadView 加载 view,因此构成了死循环,程序即卡死,所以我们常在 ViewDidLoad 里创建 view。
2.2 ViewController 卸载 View 过程
从图中可以看到,当系统发出内存警告时,会调用 didReceiveMemoeryWarning 方法,如果当前有能被释放的 view,系统会调用 viewWillUnload 方法来释放 view,完成后调用 viewDidUnload方法,至此,view 就被卸载了。此时原本指向 view 的变量要被置为 nil,具体操作是在 viewDidUnload 方法中调用 self.myButton = nil。
3.viewDidLoad
当控制器的 loadView 方法执行完毕,view 被创建成功后,就会执行 viewDidLoad 方法,该方法与loadView 方法一样,也有可能被执行多次。在开发中,我们可能从未遇到过执行多次的情况,那什么时候会执行多次呢?
比如 A 控制器 push 出 B 控制器,此时,窗口显示的是 B 控制器的 view,此时如果收到内存警告,我们一般会将 A 控制器中没用的变量及 view 销毁掉,之后当我们从 B 控制器 pop 到 A 控制器时,就会再次执行A控制器的 loadView 方法与 viewDidLoad 方法。
4.viewWillAppear && viewDidAppear
4.1 viewWillAppear
viewWillAppear 总是在 viewDidLoad 之后被调用,但不是立即,当你只是引用了属性 view,却没有立即把 view 添加到任何已经展示的视图上时,viewWillAppear 不会被调用,这在 view 被外部引用时,就会发生。当然,随着 ViewController 的多次推入,多次进入子页面后返回,该方法会被多次调用。与 viewDidLoad 不同,调用该方法就说明控制器一定会显示。
锁屏之后会被调用吗?
不会。viewWillAppear 关注的是 view 在层次中的显示与消失,锁屏并没有改变 App 本身的层次。
Window叠加后,会被调用吗?
不会。同锁屏时的原因类似,叠加 Window 并没有改变 ViewController 所在 Window 的视图层次,换句话说,view 并没有被覆盖或删除 (相对于自己所在 Window)。
注意
如果控制器 A 被展示在另一个控制器 B 的 popover 中,那么控制器 B 不会调用该方法,直到控制器 A 清除。
4.2 viewDidAppear
视图已在屏幕上渲染完成。子视图有自定义动画时,建议在 Did 方法中启动,在 Will 中启动动画时,动画效果将不会很理想。
5.viewWillAppear 与 viewDidAppear 之间发生了什么
以下两个方法将会被调用:
- viewWillLayoutSubviews- viewDidLayoutSubviews复制代码
viewWillLayoutSubviews
该方法在通知控制器将要布局 view 的子控件时调用。每当视图的 bounds 改变,view 将调整其子控件位置。默认实现为空,可重写以在 view 布局子控件前做出改变。该方法调用时,AutoLayout 未起作用。
viewDidLayoutSubviews
该方法在通知控制器已经布局 view 的子控件时调用。默认实现为空,可重写以在 view 布局子控件后做出改变。该方法调用时,AutoLayout 未起作用。
注意
使用 Autolayout 时,子视图大小只有在 viewDidLayoutSubviews 才真正被设置好,所以这里才是获取子视图大小的正确位置,常见的错误是,在 viewDidLoad 中读取了某个 view.frame,用来给其它子视图赋值,结果得到一堆大小“不定”的视图,甚至可能为零,在视图中看不见!
6.viewWillDisappear && viewDidDisappear
viewWillDisappear
该方法在控制器 view 将要从视图层次移除时被调用,可重写以提交变更,取消视图第一响应者状态。
viewDidDisappear
该方法在控制器 view 已经从视图层次移除时被调用,可重写以清除或隐藏控件。
两者配套调用,具体指子视图控制器是以 push 和 present 方法显示的,父视图控制器的以上两个方法会被触发。
特别的,addSubview 会调用子控制器 Appear 系列方法,但不会调用父视图 viewWillDisappear 方法。
如下添加子视图:
XSDViewController *subVC = [[XSDViewController alloc] init];[self addChildViewController:subVC];[subVC.view setFrame:self.view.frame];[self.view addSubview:subVC.view];[subVC didMoveToParentViewController:self];复制代码
得到结果是,只有 XSDViewController 的 Appear 系列方法被调用,这样的调用与 push / present 方法根本不同是父视图的 view 没有“隐藏”,只是被覆盖了。
7.didReceiveMemoryWarning && viewDidUnload (iOS6废除)
当系统内存不足时,首先 ViewController 的 didReceiveMemoryWarining 方法会被调用,而 didReceiveMemoryWarining 会判断当前 ViewController 的 view 是否显示在 window 上,如果没有显示在 window 上,则 didReceiveMemoryWarining 会自动将 ViewController 的 view 以及其所有子 view 全部销毁,然后调用 viewcontroller 的 viewdidunload 方法。如果当前 ViewController 的 view 显示在 window 上,则不销毁该 ViewController 的 view,当然,viewDidunload 也不会被调用了。
iOS 升级到 6.0 以后,不再支持 viewDidUnload 了。官方文档的解释是系统会自动控制大的 view 所占用的内存,其他小的 view 所占用的内存是极其微小的,不值得为了省内存而去清理然后在重新创建。如果你需要在内存警告的时候释放业务数据或者做些其他的特定处理,你可以实现 didReceiveMemoryWarning 这个函数。
iOS 6.0 及以上版本的内存警告处理方法:
-(void)didReceiveMemoryWarning {[super didReceiveMemoryWarning];//即使没有显示在window上,也不会自动的将self.view释放。 // Dispose of any resources that can be recreated.// 此处做兼容处理需要加上ios6.0的宏开关,保证是在6.0下使用的,6.0以前屏蔽以下代码,否则会在下面使用self.view时自动加载viewDidUnLoadif ([[UIDevice currentDevice].systemVersion floatValue] >= 6.0) {//需要注意的是self.isViewLoaded是必不可少的,其他方式访问视图会导致它加载 ,在WWDC视频也忽视这一点。if (self.isViewLoaded && !self.view.window) {// 是否是正在使用的视图//codeself.view = nil;// 目的是再次进入时能够重新加载调用viewDidLoad函数。}}}复制代码
8.dealloc
当发出内存警告调用 viewDidUnload 方法时,只是释放了 view,并没有释放 ViewController,所以并不会调用 dealloc 方法。即 viewDidUnload 和 dealloc 方法并没有任何关系,dealloc 方法只会在 ViewController 被释放的时候调用。
9.其他相关方法
awakeFromNib
当 .nib 文件被加载的时候,会发送一个 awakeFromNib 的消息到 .nib 文件中的每个对象,每个对象都可以定义自己的 awakeFromNib 方法来响应这个消息,执行一些必要的操作。也就是说通过 nib 文件创建 view 对象时执行 awakeFromNib。
看完文档继续补充。
10.多个 ViewControllers 跳转时的生命周期
10.1 Push / Present
当我们点击 push 的时候首先会加载下一个界面然后才会调用界面的消失方法。
init:ViewController2
loadView:ViewController2
viewDidLoad:ViewController2
viewWillDisappear:ViewController1 将要消失
viewWillAppear:ViewController2 将要出现
viewWillLayoutSubviews ViewController2
viewDidLayoutSubviews ViewController2
viewWillLayoutSubviews:ViewController1
viewDidLayoutSubviews:ViewController1
viewDidDisappear:ViewController1 完全消失
viewDidAppear:ViewController2 完全出现
当在一个控制器内 Push / Present 新的控制器,原先的控制器并不会销毁,但会消失,因此调用了 viewWillDisappear 和 viewDidDisappear 方法。
10.2 Pop / Dismiss
如果控制器 A 被展示在另一个控制器 B 的 popover 中,那么控制器 B 不会调用 viewWillAppear 方法,直到控制器 A 清除。这时,控制器 B 会再一次出现,因此调用了其中的 viewWillAppear 和 viewDidAppear 方法。