iOS APP 启动流程及 UI 生命周期

这是 iOS 开发的基础,也是必须要了解的内容。

一. iOS 程序的启动流程

当用户点击应用图标之后,应用就开始启动。应用启动完成后,就会展示一系列的视图,和用户进行各种各样的交互(如滑动、点击)。当用户退出应用后,该应用就无法和用户进行交互。这一过程就体现了一个应用的生命周期(从启动到退出)。

UIApplication

UIApplication 对象是应用程序的象征,一个 UIApplication 对象就代表一个应用程序。每一个应用都有自己的 UIApplication 对象,而且是单例的,如果试图在程序中新建一个UIApplication 对象,那么将报错提示。一个 iOS 程序启动后创建的第一个对象就是 UIApplication 对象,且只有一个。利用 UIApplication 对象,能进行一些应用级别的操作。

在 UIApplicationMain 函数中,根据传入的 UIApplication 名称和它的代理的名称,会主要做下面的事情:

  • 根据传入的名称创建 UIApplication 对象
  • 根据传入的代理名称创建 UIApplication 代理对象
  • 开启事件循环(如果不进行循环,那么在 main 函数结束后程序就结束了。要保证程序创建后可以一直存在)
  • 解析 Info.plist 文件:会在 Info.plist 文件里查找 Main storyboard file base name 这个 Key 对应的 Value 是否有值。如果有值,则表示之后会通过 Storyboard 加载控制器,AppDelegate 会接收到 didFinishLaunchingWithOptions 消息(程序启动完成的时候),此时 Storyboard 会进行一系列的加载操作(后面会具体说);如果没有值,则不会通过 Storyboard 加载控制器,接着 AppDelegate 会接收到 didFinishLaunchingWithOptions 消息(程序启动完成的时候),在这个时候需要我们通过代码的方式加载控制器。注意 Info.plist 中 Main storyboard file base name 这个 Key 并不是真正的 Key,而是苹果为了增强可读性才这样写的,真正的 Key 为 UIMainStoryboardFile(可以通过 Info.plist 文件的源代码查看)。

UIApplication Delegate

所有的移动操作系统都有个致命的缺点:APP 很容易受到打扰。比如一个来电或者锁屏会导致 APP 进入后台甚至被终止。还有很多其它类似的情况会导致 APP 受到干扰,在 APP 受到干扰时,会产生一些系统事件,这时 UIApplication 会通知它的 delegate 对象,让 delegate 代理来处理这些系统事件。

所有 UIApplication Delegate 的作用是当应用程序发出一系列系统事件时,做出相应的反应。每次新建完项目,都有个带有“AppDelegate”字眼的类,它就是 UIApplication 的代理,AppDelegate 默认已经遵守了 UIApplicationDelegate 协议,已经是 UIApplication 的代理。系统事件如程序降将要启动、程序启动完成、程序进入后台、程序进入前台、程序退出等。

具体执行流程

  • 程序入口
    进入main函数,设置 AppDelegate 称为函数的代理
    程序完成加载 :[AppDelegate application:didFinishLaunchingWithOptions:]

  • 创建window窗口
    程序被激活:[AppDelegate applicationDidBecomeActive:]-当点击command+H时(针对模拟器,手机是当点击home键)
    程序取消激活状态:[AppDelegate applicationWillResignActive:]
    程序进入后台:[AppDelegate applicationDidEnterBackground:]

程序进入前台:[AppDelegate applicationWillEnterForeground:]
程序被激活:[AppDelegate applicationDidBecomeActive:]

分析

对于applicationWillResignActive(非活动)与applicationDidEnterBackground(后台)这两个的区别
  • applicationWillResignActive(非活动):

    比如当有电话进来或短信进来或锁屏等情况下,这时应用程序挂起进入非活动状态,也就是手机界面还是显示着你当前的应用程序的窗口,只不过被别的任务强制占用了,也可能是即将进入后台状态(因为要先进入非活动状态然后进入后台状态)

  • applicationDidEnterBackground(后台):

    指当前窗口不是你的App,大多数程序进入这个后台会在这个状态上停留一会,时间到之后会进入挂起状态(Suspended)。如果你程序特殊处理后可以长期处于后台状态也可以运行。

  • Suspended (挂起):

    程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存。

UIApplicationMain 函数解释:

入口函数:

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

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

argc 和 argv 参数是为了与 C 语言保持一致。
principalClassName (主要类名) 和 ** delegateClassName (委托类名)**。

  • 如果 principalClassName 是 nil,那么它的值将从 Info.plist 去获取,如果 Info.plist 没有,则默认为 UIApplication。principalClass 这个类除了管理整个程序的生命周期之外什么都不做,它只负责监听事件然后交给 delegateClass 去做。
  • delegateClass 将在工程新建时实例化一个对象。NSStringFromClass([AppDelegate class])

二、UI 的生命周期

iOS APP 的 UI 是基于 UIViewController 搭建出来的,而 UIViewController 是 UIKit 框架中 Controller 部分的基础。

UIViewController 的生命周期

生命周期指对象从创建到被销毁的整个过程,保持内存的平衡和而程序高效运行,当需要一个对象时,他会被创建并分配内存空间,当它不再被需要时,应该被系统释放回收,
UIViewController 对象从创建到释放过程中会依次调用许多生命周期函数。

UIViewController的视图层级结构

UIViewController 自带一个 UIView 类型的 view 视图,这个 view 平铺在屏幕上,是视图控制器的根视图,在视图控制器中添加 UI 控件都是添加在这个 view 上,UIView 通过 addSubview 方法添加子 view 视图子 view 视图也可以继续使用 addsubview 添加自己的子视图。

状态 说明
[ViewController initWithCoder:] 或者 [ViewController initWithNibName:Bundle] 从归档文件中加载 UIViewController 对象,即使是纯代码,也会把 nil 作为参数传给后者
[ViewController awakeFromNib] 作为第一个方法的助手,方便处理一些额外的设置
[ViewController loadView] 创建或加载一个 view 并把它赋值给 UIViewController 的 view 属性
[ViewController viewDidLoad] 此时整个视图层次(view hierarchy)已经被放到内存中,可以移除一些视图,修改约束,加载数据等
[ViewController viewWillAppear:] 视图加载完成,并即将显示在屏幕上,还没有设置动画,可以改变当前屏幕方向或状态栏的风格等。
[ViewController viewWillLayoutSubviews] 用于通知视图的位置布局已经完成
[ViewController viewDidAppear:] 视图已经展示在屏幕上,可以对视图做一些关于展示效果方面的修改
[ViewController viewWillDisappear:] 视图即将消失
[ViewController viewDidDisappear:] 视图已经消失

从流程看 UIViewController 创建时的生命周期函数调用顺序

  • initialize:

    并不会在每次创建对象时被调用,只在这个类第一次创建对象时被调用做一些类的准备工作;如果有继承的子类未实现 initialize 方法,第一次创建子类对象时,父类会代替子类调用 initialize 方法

  • init和initWithCoder 都是用作初始化对象

    init:从代码进行初始化
    initWithCoder:从归档文件进行初始化

  • awakeFromNib:

    从 xib 或 storyboard 中加载 UIViewController 将要被激活时调用

  • loadView:

    加载 UI 视图初始化方法,UIViewController 生命周期中只调用一次

  • viewDidLoad:

    视图加载完成后调用,此时视图控制器基本工程已初始化完成,一般将一些 Controller 额外定义功能的初始化工作放在此函数中

  • viewWillAppear:

    视图即将显示时调用

  • viewWillLayoutSubviews:

    视图将要布局其子视图时被调用

  • viewDidLaySubviews:

    视图布局完成其子视图时被调用

  • viewDidAppear

    视图显示后被调用

UIViewController被释放和销毁的过程调用:

  • viewWillDisappear:

    在试图将要消失时调用,做一些数据保存和清理工作

  • viewDidDisappear

    视图已经消失时调用

  • dealloc或deinit

    对象的销毁方法,在对象释放时调用,可通过在其中打印信息检查是否存在内存泄露等问题

分析

  • initWithNibName:bundle:

初始化 UIViewController,执行关键数据初始化操作,非 StoryBoard 创建 UIViewController 都会调用这个方法

注意: 不要在这里做 View 相关操作,View 在 loadView 方法中才初始化。

  • initWithCoder:

如果使用 StoryBoard 进行视图管理,程序不会直接初始化一个 UIViewController,StoryBoard 会自动初始化或在 segue 被触发时自动初始化,因此方法initWithNibName:bundle 不会被调用,但是 initWithCoder 会被调用。

  • awakeFromNib

当 awakeFromNib 方法被调用时,所有视图的 outlet 和 action 已经连接,但还没有被确定,这个方法可以算作适合视图控制器的实例化配合一起使用的,因为有些需要根据用户喜好来进行设置的内容,无法存在 storyBoard 或 xib 中,所以可以在 awakeFromNib 方法中被加载进来。

  • loadView

当执行到 loadView 方法时,如果视图控制器是通过 nib 创建,那么视图控制器已经从 nib 文件中被解档并创建好了,接下来任务就是对 view 进行初始化。loadView 方法在 UIViewController 对象的 view 被访问且为空的时候调用。这是它与 awakeFromNib 方法的一个区别。

假设我们在处理内存警告时释放 view 属性:self.view = nil。因此 loadView 方法在视图控制器的生命周期内可能被调用多次。loadView 方法不应该直接被调用,而是由系统调用。它会加载或创建一个 view 并把它赋值给 UIViewController 的 view 属性。
在创建 view 的过程中,首先会根据 nibName 去找对应的 nib 文件然后加载。如果 nibName 为空或找不到对应的 nib 文件,则会创建一个空视图(这种情况一般是纯代码)

注意:在重写 loadView 方法的时候,不要调用父类的方法。

  • viewDidLoad

当 loadView 将 view 载入内存中,会进一步调用 viewDidLoad 方法来进行进一步设置。此时,视图层次已经放到内存中,通常,我们对于各种初始化数据的载入,初始设定、修改约束、移除视图等很多操作都可以这个方法中实现。

视图层次(view hierachy):因为每个视图都有自己的子视图,这个视图层次其实也可以理解为一颗树状的数据结构。而树的根节点,也就是根视图(root view),在 UIViewController 中以 view 属性。它可以看做是其他所有子视图的容器,也就是根节点。

  • viewWillAppear

系统在载入所有的数据后,将会在屏幕上显示视图,这时会先调用这个方法,通常我们会在这个方法对即将显示的视图做进一步的设置。比如,设置设备不同方向时该如何显示;设置状态栏方向、设置视图显示样式等。另一方面,当 APP 有多个视图时,上下级视图切换是也会调用这个方法,如果在调入视图时,需要对数据做更新,就只能在这个方法内实现。

  • viewWillLayoutSubviews

view 即将布局其 Subviews。 比如 view 的 bounds 改变了(例如:状态栏从不显示到显示,视图方向变化),要调整 Subviews 的位置,在调整之前要做的工作可以放在该方法中实现。

  • viewDidLayoutSubviews

view 已经布局其 Subviews,这里可以放置调整完成之后需要做的工作。

  • viewDidAppear

在 view 被添加到视图层级中以及多视图,上下级视图切换时调用这个方法,在这里可以对正在显示的视图做进一步的设置。

  • viewWillDisappear

在视图切换时,当前视图在即将被移除、或被覆盖是,会调用该方法,此时还没有调用 removeFromSuperview。

  • viewDidDisappear

view 已经消失或被覆盖,此时已经调用 removeFromSuperView;

  • dealloc

视图被销毁,此次需要对你在 init 和 viewDidLoad 中创建的对象进行释放。

  • didReceiveMemoryWarning

在内存足够的情况下,APP 的视图通常会一直保存在内存中,但是如果内存不够,一些没有正在显示的 viewController 就会收到内存不足的警告,然后就会释放自己拥有的视图,以达到释放内存的目的。但是系统只会释放内存,并不会释放对象的所有权,所以通常我们需要在这里将不需要显示在内存中保留的对象释放它的所有权,将其指针置 nil。

只有 init 系列的方法,如 initWithNibName 需要自己调用,其他方法如 loadView 和 awakeFromNib 则是系统自动调用。而 viewWill/Did 系列的方法则类似于回调和通知,也会被自动调用。
纯代码写视图布局时需要注意,要手动调用 loadView 方法,而且不要调用父类的 loadView 方法。纯代码和用 IB 的区别仅存在于 loadView 方法及其之前,编程时需要注意的也就是 loadView 方法。
除了 initWithNibName 和 awakeFromNib 方法是处理视图控制器外,其他方法都是处理视图。这两个方法在视图控制器的生命周期里只会调用一次。

三、UI 数据保存与恢复

在 iOS 开发中,我们都知道一个 APP 点击了 HOME 按键或者切换至其他应用时,将进入后台。随着时间的推移,APP 会经历后台运行,后台悬挂,最后被杀死。如

用户正在使用我们 APP 进行个人信息的编辑,突然接到了一个电话,使得 APP 进入后台并且通话时间超过了 APP 后台保活的时间。当用户通话完毕的时候,返回继续填写,却发现 APP 重新启动了,并且用户之前填写的数据,都没有保存,需要重新输入?用户的体验会很不好。

关于 UIStateRestoration

UIStateRestoration 出现于 iOS 6.0 以后的 API 中。主要帮助我们实现特定场景下的 UI 保存和恢复。UIStateRestoration 是一个协议类,在苹果的系统中 UIKit 框架下的UIApplication、UIViewController、UIView 都实现了 UIStateRestoration 协议。

关于 UI 状态从应用程序启动到恢复以及 UI 状态保存时相关 API 的调用顺序,官网的图解如下:

UI 状态保存,只有当 AppDelegate 实现 application: shouldSaveApplicationState: 并且在方法中返回 true 时才会生效。

UI 状态恢复,只有当 AppDelegate 实现 application:shouldRestoreApplicationState: 并且在方法中返回 true 时才会生效。