一、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 开始,那此方法就是结束。 -
loadView
与veiwDidLoad
在此方法中创建视图。
我们可以通过下图来理解它的逻辑:

每次访问 view
时,就会调用 self.view
的 get
方法,在 get
方法中判断self.view==nil
,不为nil
就直接返回 view
,等于 nil
就去调用 loadView
方法。loadView
方法会去判断有无指定 storyBord/Xib
文件,如果有就去加载 storyBord/Xib
描述的控制器 view
,如果没有则系统默认创建一个空的 view
,赋给 self.view
。loadView
方法有可能被多次调用(每当访问 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
配套使用。 -
viewWillLayoutSubviews
与viewDidLayoutSubviews
这两个方法发生在 viewWillAppear 与 viewDidAppear 之间。
- viewWillLayoutSubviews
控制器将要布局 view 的子控件时调用,默认实现为空。此时子控件的大小还没有设置好。
- viewDidLayoutSubviews
控制器已经布局 view 的子控件时调用,默认实现为空。此时子控件的大小才被设置好,这里才是获取子视图大小的正确位置。
-
viewWillDisappear
与viewDidDisappear
- viewWillDisappear
视图将要消失时调用
- viewDidDisappear
视图完全消失后调用
-
didReceiveMemoryWarning
与viewDidUnload
这两个方法是收到内存警告时调用的。
- 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结束后,如果有需要刷新,执行一次即可。
网友评论