App、UIViewController、UIView 生命周期

一、App的生命周期

当我们打开 APP 时,程序一般都是从 main 函数开始运行的,那么我们先来看下 Xcode 自动生成的 main.m 文件:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

这个默认的 iOS 程序就是从 main 函数开始执行的,但是在 main 函数中我们其实只能看到一个方法,这个方法内部是一个消息循环(相当于一个死循环),因此运行到这个方法 UIApplicationMain 之后程序不会自动退出,而只有当用户手动关闭程序这个循环才结束。我们看下这个方法定义:

int UIApplicationMain(int argc, char * _Nullable *argv, NSString *principalClassName, NSString *delegateClassName);

这个方法有四个参数:

  • argc:参数个数,与 main 函数的参数对应。
  • argv:参数内容,与 main 函数的参数对应。
  • principalClassName:代表 UIApplication 类或其子类。这个参数默认为 nil,则代表 UIApplication 类。UIApplication 是单例模式,一个应用程序只有一个 UIApplication 对象或子对象。
  • delegateClassName:代理,默认生成的是 AppDelegate 类,这个类主要用于监听整个应用程序生命周期的各个事件,当UIApplication运行过程中引发了某个事件之后会调用代理中对应的方法。

关于返回值,即便声明了返回值,但该函数也从不会返回。

也就是说当执行 UIApplicationMain 方法后这个方法会根据第三个参数principalClassName创建对应的 UIApplication 对象,这个对象会根据第四个参数delegateClassName 创建 AppDelegate 并指定此对象为 UIApplication 的代理;同时 UIApplication 会开启一个消息循环不断监听应用程序的各个活动,当应用程序生命周期发生改变 UIApplication 就会调用代理对应的方法。

既然应用程序 UIApplication 是通过代理和外部交互的,那么我们就有必要清楚 AppDelegate 的操作细节,在这个类中定义了生命周期的各个事件的执行方法:

#import "AppDelegate.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSLog(@"程序已经启动");
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application {
    NSLog(@"程序将要失去焦点");
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    NSLog(@"程序已经进入后台");
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    NSLog(@"程序将要进入前台");
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    NSLog(@"程序获得焦点");
}

- (void)applicationWillTerminate:(UIApplication *)application {
    NSLog(@"程序将要终止");
}

@end

简要说下我们不同的操作,程序运行结果:

  • 启动程序

程序已经启动
程序已经获得焦点

  • 按下 home 键

程序将要失去焦点
程序已经进入后台

  • 重新进入程序

程序将要进入前台
程序已经获得焦点

  • 下拉状态栏

程序将要失去焦点

  • 状态栏收回

程序已经获得焦点

  • 上拉控制中心

程序将要失去焦点

  • 收回控制中心

程序已经获得焦点

  • 来电

程序将要失去焦点

  • 断电

程序获得焦点

  • 双击 Home 并关闭应用

程序将要失去焦点
程序已经进入后台
程序将要终止

相信通过上面运行过程大家会对整个运行周期有个大概了解。比较容易混淆的地方就是应用程序进入前台、激活、失去焦点、进入后台,这几个方法大家要清楚。如果一个应用程序失去焦点那么意味着用户当前无法进行交互操作,因此一般会先失去焦点再进入后台防止进入后台过程中用户误操作;如果一个应用程序进入前台也是类似的,会先进入前台再获得焦点,这样进入前台过程中未完全准备好的情况下用户无法操作。另外一般如果应用程序要保存用户数据会在注销激活中进行(而不是在进入后台方法中进行),因为如果用户双击Home不会进入后台只会注销激活;如果用户恢复应用状态一般在进入激活状态时处理(而不是在进入前台方法中进行),因为用户可能是从任务栏直接返回应用,此时不会执行进入前台操作。

当然,上面的事件并不是所有AppDelegate事件,而是最常用的一些事件,其他事件大家可以查阅官方文档,例如-(void)applicationDidReceiveMemoryWarning:(UIApplication *)application;用于在内存占用过多发出内存警告时调用并通知对应的ViewController调用其内存回收方法。这里简单以图形方式描述一下应用程序的调用过程:


二、UIViewController 的生命周期

先上经典图


#import "TestViewController.h"

@interface TestViewController ()

@end

@implementation TestViewController

-(instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
   self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    NSLog(@"%s",__func__);
    return self;
}

-(instancetype)init{
    self = [super init];
    NSLog(@"%s",__func__);
    return self;
}

-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    self = [super initWithCoder:aDecoder];
    NSLog(@"%s",__func__);
    return self;
}

-(void)awakeFromNib{
    [super awakeFromNib];
    NSLog(@"%s",__func__);
}

-(void)loadView{
    [super loadView];
    NSLog(@"%s",__func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%s",__func__);
}

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    NSLog(@"%s",__func__);
}

-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    NSLog(@"%s",__func__);
}

-(void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];
    NSLog(@"%s",__func__);
}

-(void)viewDidLayoutSubviews{
    [super viewDidLayoutSubviews];
    NSLog(@"%s",__func__);
}

-(void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    NSLog(@"%s",__func__);
}

-(void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
    NSLog(@"%s",__func__);
}

-(void)dealloc{
    NSLog(@"%s",__func__);
}

@end

我们在创建 TestViewController 实例时,可以通过以下两种方法:

//第一种
[[TestViewController alloc]initWithNibName:@"ViewController" bundle:nil];

//第二种    
[[TestViewController alloc]init];

我们经常使用的是第二种创建方法,其实第二种方法默认实现了第一种的方法,只不过两个参数默认传的是 nil。

当 TestVeiwController 通过 xib 加载的时候,看下 viewDidLoad 之前发生了什么:

-[TestViewController initWithNibName:bundle:]
-[TestViewController init]
-[TestViewController loadView]
-[TestViewController viewDidLoad]

无 xib:

-[TestViewController initWithNibName:bundle:]
-[TestViewController init]
-[TestViewController loadView]
-[TestViewController viewDidLoad]

TestVeiwController 通过 storyboard 加载:

-[TestViewController initWithCoder:]
-[TestViewController awakeFromNib]
-[TestViewController loadView]
-[TestViewController viewDidLoad]

我们可以看到通过 storyboard 实例化与 init 实例化在 loadView方法调用之前走的是不同的方法。我们看下这几个方法的不同:

  • initWithNibName:bundle:
    此方法发生在 nib 加载之前。

调用此方法进行 Controller 初始化,与 nib 加载无关。nib 的加载是懒加载,当 Controller 需要加载其视图时,才会加载此方法中指定的 nib。
可以看出该方法初始化的 Controller 不是从 nib 创建的。

  • initWithCoder
    此方法发生在 nib 加载期间。
    所有 archived 对象的初始化使用此方法。nib 中存储的对象就是 archived 对象,所以此方法是 nib 加载对象时使用的初始化方法。
    当从 nib 创建 UIViewController 时使用此方法。

  • awakeFromNib
    此方法发生在 nib 中所有对象都已完全加载完之后。
    如果 initWithCoder是 unarchiving 开始,那此方法就是结束。

  • loadViewveiwDidLoad
    在此方法中创建视图。

我们可以通过下图来理解它的逻辑:


每次访问 view 时,就会调用 self.viewget 方法,在 get 方法中判断self.view==nil,不为nil就直接返回 view,等于 nil就去调用 loadView 方法。loadView 方法会去判断有无指定 storyBord/Xib 文件,如果有就去加载 storyBord/Xib 描述的控制器 view,如果没有则系统默认创建一个空的 view,赋给 self.viewloadView方法有可能被多次调用(每当访问 self.view 并且为 nil时就会调用一次);

系统会自动为我们加载 view,我们完全没必要手动创建 view

  • viewWillAppear
    视图将要被展示的时候调用。

其调用的时机与视图所在层次有关。例如我们常用的 push 与 present 操作改变了当前视图层次,都会触发此方法。

1、那么 UIAlertController 也是 present 操作怎么没有触发呢?

因为 UIAlertController 在另一个 window上,view 在自己所在的 window 中层次并没有改变,所以不会触发,同理在锁屏以及进入后台时也不会触发。

2、如果控制器 B 被展示在另一个控制器 A 的 popover 中,那么被展示的控制器 B 在消失后,控制器 A 并不会调用此方法。

官方原文:

If a view controller is presented by a view controller inside of a popover, this method is not invoked on the presenting view controller after the presented controller is dismissed.

例如我们使用的addSubview方法,如下:

AViewController.m 中:

BViewController *B = [[BViewController alloc]init];
[self addChildViewController:B];
[self.view addSubview:B.view];

当我们将 BViewController 从 AViewController 中移除后,并不会触发 AViewController 的 viewWillAppear 方法。

  • viewDidAppear
    视图渲染完成后调用,与viewWillAppear配套使用。

  • viewWillLayoutSubviewsviewDidLayoutSubviews

这两个方法发生在 viewWillAppear 与 viewDidAppear 之间。

  • viewWillLayoutSubviews
    控制器将要布局 view 的子控件时调用,默认实现为空。此时子控件的大小还没有设置好。
  • viewDidLayoutSubviews
    控制器已经布局 view 的子控件时调用,默认实现为空。此时子控件的大小才被设置好,这里才是获取子视图大小的正确位置。
  • viewWillDisappearviewDidDisappear
  • viewWillDisappear
    视图将要消失时调用
  • viewDidDisappear
    视图完全消失后调用
  • didReceiveMemoryWarningviewDidUnload

这两个方法是收到内存警告时调用的。

  • viewDidUnload
    在 iOS5 以及之前使用的方法,iOS6 及之后已经废弃。在收到内存警告时,在此方法中将 view 置为 nil;
  • didReceiveMemoryWarning
    收到内存警告时,系统自动调用此方法,回收占用大量内存的视图数据。我们一般不需要在这里做额外的操作。如果要自己处理一些额外内存,重写时需要调用父类方法,即[super didReceiveMemoryWarning]
  • dealloc
    UIViewController 释放时调用此方法。UIViewController 的生命周期到此结束。
    当我们重写此方法时,ARC 环境下不需要调用父类方法,MRC 环境下需要调用父类方法,即[super dealloc]

三、UIView 的生命周期

UIView生命周期相关函数:

//构造方法,初始化时调用,不会调用init方法
- (instancetype)initWithFrame:(CGRect)frame;
//添加子控件时调用
- (void)didAddSubview:(UIView *)subview ;
//构造方法,内部会调用initWithFrame方法
- (instancetype)init;
//xib归档初始化视图后调用,如果xib中添加了子控件会在didAddSubview方法调用后调用
- (instancetype)initWithCoder:(NSCoder *)aDecoder;
//唤醒xib,可以布局子控件
- (void)awakeFromNib;
//父视图将要更改为指定的父视图,当前视图被添加到父视图时调用
- (void)willMoveToSuperview:(UIView *)newSuperview;
//父视图已更改
- (void)didMoveToSuperview;
//其窗口对象将要更改
- (void)willMoveToWindow:(UIWindow *)newWindow;
//窗口对象已经更改
- (void)didMoveToWindow;
//布局子控件
- (void)layoutSubviews;
//绘制视图
- (void)drawRect:(CGRect)rect;
//从父控件中移除
- (void)removeFromSuperview;
//销毁
- (void)dealloc;
//将要移除子控件
- (void)willRemoveSubview:(UIView *)subview;

1.没有子控件的UIView

显示过程:

//(superview)
- (void)willmovetosuperview:(nullable UIView *)newSuperview
- (void)didmovetosuperview

//(window)
- (void)willmovetowindow:(nullable UIWindow *)newWindow
- (void)didmovetowindow

- (void)layoutsubviews

移出过程:

//(window)
- (void)willmovetowindow:(nullable UIWindow *)newWindow
- (void)didmovetowindow

//(superview)
- (void)willmovetosuperview:(nullable UIView *)newSuperview
- (void)didmovetosuperview

- (void)removeFromSuperview
- (void)dealloc

但是在移出时newWindow和newSuperview 都是nil。

2.包含子控件的UIView

当增加一个子控件时,就会执行 didAddSubview,之后也会执行一次layoutsubview
在view释放后,执行完,dealloc就会多次执行willRemoveSubview.先add的view,先释放掉。

3.layoutsubview

在上面的方法中,经常发现layoutsubview会被调用,下面说下layoutsubview的调用情况:
1、addSubview会触发layoutSubviews,如果addSubview 如果连续2个 只会执行一次,具体原因下面说。
2、设置view的Frame会触发layoutSubviews,必须是frame的值设置前后发生了变化。
3、滚动一个UIScrollView会触发layoutSubviews。
4、旋转Screen会触发父UIView上的layoutSubviews事件。
5、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。

TIP
1、如果要立即执行layoutsubview,
要先调用[view setNeedsLayout],把标记设为需要布局.
然后马上调用[view layoutIfNeeded],实现布局.

其中的原理是:执行setNeedsLayout后会在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews。
这样刷新会产生延迟,所以我们需要马上执行layoutIfNeeded。就会开始遍历subviews的链,判断该receiver是否需要layout。如果需要立即执行layoutsubview

2、addSubview

每一个视图只能有唯一的一个父视图。如果当前操作视图已经有另外的一个父视图,则addsubview的操作会把它先从上一个父视图中移除(包括响应者链),再加到新的父视图上面。

连续2次的addSubview,只会执行一次layoutsubview。因为一次的runLoop结束后,如果有需要刷新,执行一次即可。

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

推荐阅读更多精彩内容