一、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结束后,如果有需要刷新,执行一次即可。