iOS启动流程和生命周期

1. 启动流程

1.1 准备知识

Mach-O
Executable 可执行文件
Dylib 动态库
Bundle 无法被连接的动态库,只能通过dlopen()加载
Image 指的是Executable,Dylib或者Bundle的一种
Framework 动态库和对应的头文件和资源文件的集合

Apple的操作系统的可执行文件格式几乎都是mach-o,mach-o可以大致的分为三部分:

Header 头部包含可以执行的CPU架构,比如x86,arm64
Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
Data 数据,包含load commands中需要的各个段(segment)的数据,每一个Segment都得大小是Page的整数倍。

绝大多数mach-o包括以下三个段(支持用户自定义Segment,但是很少使用)

__TEXT 代码段 只读,包括函数,和只读的字符串,上图中类似__TEXT,__text的都是代码段
__DATA 数据段 读写,包括可读写的全局变量等,上图类似中的__DATA,__data都是数据段
__LINKEDIT 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。
dyld

dyld的全称是dynamic loader,它的作用是加载一个进程所需要的image,它是开源的。

  • Virtual Memory

    虚拟内存是在物理内存上建立的一个逻辑地址空间,它向上(应用)提供了一个连续的逻辑地址空间,向下隐藏了物理内存的细节。
    虚拟内存使得逻辑地址可以没有实际的物理地址,也可以让多个逻辑地址对应到一个物理地址。

    虚拟内存被划分为一个个大小相同的Page(64位系统上是16KB),提高管理和读写的效率。 Page又分为只读和读写的Page。

    虚拟内存是建立在物理内存和进程之间的中间层。在iOS上,当内存不足的时候,会尝试释放那些只读的Page,因为只读的Page在下次被访问的时候,可以再从磁盘读取。如果没有可用内存,会通知在后台的App(也就是在这个时候收到了memory warning),如果在这之后仍然没有可用内存,则会杀死在后台的App。

  • Page fault
    在应用执行的时候,它被分配的逻辑地址空间都是可以访问的,当应用访问一个逻辑Page,而在对应的物理内存中并不存在的时候,这时候就发生了一次Page fault。当Page fault发生的时候,会中断当前的程序,在物理内存中寻找一个可用的Page,然后从磁盘中读取数据到物理内存,接着继续执行当前程序。

  • Dirty Page & Clean Page
    如果一个Page可以从磁盘上重新生成,那么这个Page称为Clean Page
    如果一个Page包含了进程相关信息,那么这个Page称为Dirty Page
    像代码段这种只读的Page就是Clean Page。而像数据段(_DATA)这种读写的Page,当写数据发生的时候,会触发COW(Copy on write),也就是写时复制,Page会被标记成Dirty,同时会被复制。

1.2 dyld2启动流程

dyld2启动流程
加载dyld到App进程
加载动态库(包括所依赖的所有动态库)
Rebase
Bind
初始化Objective-C Runtime
其它的初始化代码
加载动态库

dyld会首先读取mach-o文件的Header和load commands。
接着就知道了这个可执行文件依赖的动态库。例如加载动态库A到内存,接着检查A所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个App所依赖的动态库在100-400个左右,其中大多数都是系统的动态库,它们会被缓存到dyld shared cache,这样读取的效率会很高。

查看mach-o文件所依赖的动态库,可以通过MachOView的图形化界面(展开Load Command就能看到),也可以通过命令行otool。

Rebase && Bind

有两种主要的技术来保证应用的安全:ASLR和Code Sign。

ASLR的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。

Code Sign相信大多数开发者都知晓,这里要提一点的是,在进行Code sign的时候,加密哈希不是针对于整个文件,而是针对于每一个Page的。这就保证了在dyld进行加载的时候,可以对每一个page进行独立的验证。

mach-o中有很多符号,有指向当前mach-o的,也有指向其他dylib的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?

mach-o中采用了PIC技术,全称是Position Independ code。当你的程序要调用printf的时候,会先在__DATA段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分

Rebase 修正内部(指向当前mach-o文件)的指针指向
Bind 修正外部指针指向

之所以需要Rebase,是因为刚刚提到的ASLR使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在__LINKEDIT中。

Rebase解决了内部的符号引用问题,而外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的。

Objective-C

Objective C是动态语言,所以在执行main函数之前,需要把类的信息注册到一个全局的Table中。同时,Objective C支持Category,在初始化的时候,也会把Category中的方法注册到对应的类中,同时会唯一Selector,这也是为什么当你的Cagegory实现了类中同名的方法后,类中的方法会被覆盖。

另外,由于iOS开发是基于Cocoa Touch的,所以绝大多数的类起始都是系统类,所以大多数的Runtime初始化起始在Rebase和Bind中已经完成。

Initializers

接下来就是必要的初始化部分了,主要包括几部分:

  • load(Swift已弃用,只能使用initialize)
  • C/C++静态初始化对象和标记为__attribute__(constructor)的方法

1.3 dyld3启动流程

上文的讲解是dyld2的加载方式。而最新的是dyld3加载方式略有不同:

加载方式 process 注释
dyld2 in-process 只有当应用程序被启动的时候,dyld2才能开始执行任务。
dyld3 部分out-of-process和in-process。 out-of-process在App下载安装和版本更新的时候会去执行。

out-of-process会做如下事情:

  • 分析Mach-o Headers
  • 分析依赖的动态库
  • 查找需要Rebase & Bind之类的符号
  • 把上述结果写入缓存

这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。

1.4 main之后

相对于开发者来说,main才是程序入口。下面是加载流程:

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

UIApplicationMain函数有四个参数,最后一个参数是AppDelegate的类名,通常使用模板创建的AppDelegate是AppDelegate,如果我们想要改变它的名字,我们同样需要在这里传入对应的类名。

 UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);
参数名 作用
argcargv ISO C标准的main函数的参数,直接传递给UIApplicationMain进行相关处理。参数包含应用程序何时从系统启动等信息。这些参数是由UIKit的基础设施解析,否则可以忽略不计。该参数一般不会修改
principalClassName 这个参数标识了应用程序的类的名称(该类必须继承自UIApplication类)。这是负责运行应用程序的类。建议为这个参数传nil。如果principalClassName是nil,那么它的值将从Info.plist去获取,如果Info.plist没有,则默认为UIApplication。principalClass这个类除了管理整个程序的生命周期之外什么都不做,它只负责监听事件然后交给delegateClass去做。该参数一般使用nil
delegateClassName delegateClass是应用程序类的代理类。应用程序的代理负责管理系统和你的代码之间的高层次的互动。
    1. 程序完成加载
      我们一般会在这里进行一些初始化配置,例如创建window
- [AppDelegate application:didFinishLaunchingWithOptions:]
    1. 创建window窗口
      我们所有的画面最终都会显示在该窗口上,makeKeyAndVisible是window显示的关键。
_window = UIWindow.new;
_window.backgroundColor = [UIColor whiteColor];
[_window makeKeyAndVisible];
    1. 程序被激活
      最后该方法会被调用,宣布程序处于激活状态。
- [AppDelegate applicationDidBecomeActive:]

2. AppDelegate

AppDelegate类有以下常用的函数,这里是我们与系统进行交互的场所,一般在这里创建视图以及监听部分设备状态。

 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSLog(@"%s",__func__);
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application {
     NSLog(@"%s",__func__);
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
   NSLog(@"%s",__func__);
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
   NSLog(@"%s",__func__);
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
  NSLog(@"%s",__func__);
}

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
     NSLog(@"%s",__func__);
}

- (void)applicationWillTerminate:(UIApplication *)application {
    NSLog(@"%s",__func__);
}

APP状态更改后会收到一些通知。

// 启动APP会调用
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[AppDelegate applicationDidBecomeActive:]

// 点击Home键会调用
-[AppDelegate applicationWillResignActive:]
-[AppDelegate applicationDidEnterBackground:]

// APP从后台返回前台
-[AppDelegate applicationWillEnterForeground:]
-[AppDelegate applicationDidBecomeActive:]

// 收到内存警告
-[AppDelegate applicationDidReceiveMemoryWarning:]
函数 分析 注意事项
application:willFinishLaunchingWithOptions:
application:didFinishLaunchingWithOptions:
分别是程序首次将要和已经完成启动时执行,一般在这个函数里创建window对象,将程序内容通过window呈现给用户。

①检查启动选项字典中的内容,查看程序启动的方式,并做出适当的反应。
②初始化应用程序的关键数据结构。
③准备好你的应用程序的窗口和视图进行显示。
1. 使用OpenGL ES的应用程序不应该使用这个方法来准备他们的绘图环境。相反,他们应该推迟到application:DidBecomeActive:方法调用时启动OpenGL ES绘图方法。

2. 您的应用程序方法应该总是尽可能为轻量,以减少你的应用程序的启动时间。应用预期将启动并初始化自身,并开始处理不到5秒的事件。如果一个应用程序没有及时完成它的启动周期,系统会杀死它。因此,有可能你的启动慢下来(如接入网络)的任何任务,应在异步辅助线程执行。

3. 当程序启动到前台,该系统还会调用applicationDidBecomeActive:方法来完成过渡到前台。因为这种方法既在启动时与从后台过渡到前台时被调用,使用它来执行所共有的两个转变的任何任务。
applicationWillResignActive 程序将要失去Active状态时调用,比如有电话进来或者按下Home键,之后程序进入后台状态,对应的applicationWillEnterForeground(即将进入前台)方法。 该函数里面主要执行操作:

a . 暂停正在执行的任务
b. 禁止计时器
c. 减少OpenGL ES帧率
d. 若为游戏应暂停游戏
applicationDidEnterBackground 该方法用来:

a. 释放共享资源
b. 保存用户数据(写到硬盘)
c. 作废计时器
d. 保存足够的程序状态以便下次修复;
applicationWillEnterForeground 这个方法用来: 撤销applicationWillResignActive中做的改变。
applicationDidBecomeActive 若程序之前在后台,在此方法内刷新用户界面
applicationWillTerminate 程序即将退出时调用。记得保存数据,如applicationDidEnterBackground方法一样。

3. 启动优化

如果你刚刚启动过App,这时候App的启动所需要的数据仍然在缓存中,再次启动的时候称为热启动。如果设备刚刚重启,然后启动App,这时候称为冷启动。

启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。

以main函数作为分水岭,启动时间其实包括了两部分:main函数之前和main函数到第一个界面的viewDidAppear:。不过一般情况下都是耗时都产生在自己的代码,优先考虑优化main之后的过程。

优化这些初始化的核心思想就是:能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

推荐阅读更多精彩内容