iOS开发笔记:UIViewController的生命周期

经常会用到 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 方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容