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