【iOS】——应用启动流程
启动概要
启动的定义
-
广义上的启动是点击图标到首页数据加载完毕
-
狭义上的启动是点击图标到启动图完全消失的第一帧
启动的最佳时间是400ms以内,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉
启动的计算方式如下:
- 起点:进程创建的时间
- 终点:第一个
CA::Transaction::commit()
CATransaction
是 Core Animation 提供的一种事务机制,把一组 UI 上的修改打包,一起发给 Render Server 渲染。
启动的种类
启动主要分为两类:
冷启动:系统里没有任何进程的缓存信息,典型的是重启手机后直接启动 App
热启动:如果把 App 进程杀了,然后立刻重新启动,这次启动就是热启动,因为进程缓存还在
线上用户的冷启动多还是热启动多呢?
答案是和产品形态有关系,打开频次越高,热启动比例就越高。
iOS应用启动流程
(1)解析info.plist
- 加载相关信息,例如如闪屏
- 沙箱建立、权限检查
闪屏是指在应用程序启动时,用户看到的第一个屏幕或界面
(2)加载Mach-O
-
dylib loading time(动态库耗时)
-
rebase/binding time(偏移修正/符号绑定耗时)
-
加载类扩展(Category)中的方法
-
C++静态对象加载、调用ObjC的 +load 函数
-
执行声明为__attribute__((constructor))的C函数
rebase(偏移修正):任何一个app生成的二进制文件,在二进制文件内部所有的方法、函数调用,都有一个地址,这个地址是在当前二进制文件中的偏移地址。一旦在运行时刻(即运行到内存中),每次系统都会随机分配一个ASLR(Address Space Layout Randomization,地址空间布局随机化)地址值(是一个安全机制,会分配一个随机的数值,插入在二进制文件的开头),例如,二进制文件中有一个 test方法,偏移值是0x0001,而随机分配的ASLR是0x1f00,如果想访问test方法,其内存地址(即真实地址)变为 ASLR+偏移值 = 运行时确定的内存地址(即0x1f00+0x0001 = 0x1f01)。程序每次启动后地址都会随机变化,这样程序里所有的代码地址都需要需要重新对进行计算修复才能正常访问。rebasing这一步主要就是调整镜像内部指针的指向。
binding(绑定):,例如NSLog方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld做的,也称为动态库符号绑定),一句话概括:绑定就是给符号赋值的过程
(3)执行程序
- 调用main()
- 调用UIApplicationMain()
- 调用applicationWillFinishLaunching
iOS应用启动主要阶段
主要分为两个阶段,pre-main阶段和main阶段
pre-main阶段:程序启动到main函数执行前
main阶段:在执行main函数后,调用AppDelegate中的-application:didFinishLaunchingWithOptions:
方法完成初始化,并展示首页
pre-main阶段
-
加载应用的可执行文件。
-
加载动态链接库加载器dyld(dynamic loader)。
-
dyld递归加载应用所有依赖的dylib(dynamic library 动态链接库)。
-
进行**
rebase
指针调整和bind
**符号绑定。 -
ObjC
的runtime
初始化(ObjC setup):ObjC
相关Class
的注册、category
注册、selector
唯一性检查等。 -
初始化(Initializers):执行
+load()
方法、用attribute((constructor))
修饰的函数的调用、创建C++
静态全局变量等。
加载链接库
从主执行文件的 header
获取到需要加载的所依赖动态库列表,而 header
早就被内核映射过。然后它需要找到每个 dylib
,然后打开文件读取文件起始位置,确保它是 Mach-O
文件。接着会找到代码签名并将其注册到内核。然后在 dylib
文件的每个 segment
上调用 mmap()
。应用所依赖的 dylib
文件可能会再依赖其他 dylib
,所以 dyld
所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载 100
到 400
个 dylib
文件,但大部分都是系统 dylib
,它们会被预先计算和缓存起来,加载速度很快。
修正
在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是 Fix-ups
。代码签名使得我们不能修改指令,那样就不能让一个 dylib
的调用另一个 dylib
。这时需要加很多间接层。 现代 code-gen
被叫做动态 PIC(Position Independent Code),意味着代码可以被加载到间接的地址上。当调用发生时,code-gen
实际上会在 __DATA
段中创建一个指向被调用者的指针,然后加载指针并跳转过去。所以 dyld
做的事情就是修正(fix-up
)指针和数据。Fix-up
有两种类型,rebasing
和 binding
在执行main函数之前,需要把类的信息注册到一个全局的Table中。同时,Objective C支持Category,在初始化的时候,也会把Category中的方法注册到对应的类中,同时会唯一Selector,这也是为什么当你的Cagegory实现了类中同名的方法后,类中的方法会被覆盖。
另外,由于iOS开发时基于Cocoa Touch的,所以绝大多数的类起始都是系统类,所以大多数的Runtime初始化起始在Rebase和Bind中已经完成。
初始化
主要包括几部分:
+load方法。
C/C++静态初始化对象和标记为__attribute__(constructor)的方法
上文的讲解是dyld2的加载方式。而最新的是dyld3加载方式略有不同:
dyld2是纯粹的in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。
dyld3则是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情:
- 分析Mach-o Headers
- 分析依赖的动态库
- 查找需要Rebase & Bind之类的符号
- 把上述结果写入缓存
这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度
main阶段
-
执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions 初始化Window
-
初始化基础的ViewController结构(一般是UINavigationController+UITabViewController) 获取数据(Local DB/Network),展示给用户。
启动优化
pre-main阶段
dylibs 启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。
合并动态库,比如公司内部由私有Pod建立了如下动态库:XXTableView, XXHUD, XXLabel,强烈建议合并成一个XXUIKit来提高加载速度。
Rebase & Bind & Objective C Runtime
Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:
减少__DATA段中的指针数量。
合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个
删除无用的方法和类。
用initialize替代load
减少__atribute__((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。
不要创建线程
main阶段
延迟初始化那些不必要的UIViewController
。
能延迟执行的就延迟执行。比如SDK的初始化,界面的创建。 不能延迟执行的,尽量放到后台执行。比如数据读取,原始JSON数据转对象,日志发送。
+load与+initialize
1、+load
(1)+load
方法是一定会在runtime中被调用的。只要类被添加到runtime中了,就会调用+load
方法,即只要是在Compile Sources
中出现的文件总是会被装载,与这个类是否被用到无关,因此+load
方法总是在main函数之前调用。
(2)+load
方法不会覆盖。也就是说,如果子类实现了+load
方法,那么会先调用父类的+load
方法(无需手动调用super),然后又去执行子类的+load
方法。
(3)+load方法只会调用一次。
(4)+load方法执行顺序是:类 -> 子类 ->分类。而不同分类之间的执行顺序不一定,依据在Compile Sources
中出现的顺序**(先编译,则先调用,列表中在下方的为“先”)**。
(5)+load方法是函数指针调用,即遍历类中的方法列表,直接根据函数地址调用。如果子类没有实现+load方法,子类也不会自动调用父类的+load方法。
2、+initialize
(1)+initialize
方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。因此+initialize
方法总是在main函数之后调用。
(2)+initialize
方法只会调用一次。
(3)+initialize
方法实际上是一种惰性调用,如果一个类一直没被用到,那它的+initialize
方法也不会被调用,这一点有利于节约资源。
(4)+initialize
方法会覆盖。如果子类实现了+initialize
方法,就不会执行父类的了,直接执行子类本身的。如果分类实现了+initialize
方法,也不会再执行主类的。
(5)+initialize
方法的执行覆盖顺序是:分类 -> 子类 ->类。且只会有一个+initialize
方法被执行。
(6)+initialize
方法是发送消息(objc_msgSend()),如果子类没有实现+initialize
方法,也会自动调用其父类的+initialize
方法。
3、两者的异同
(1)相同点
- load和initialize会被自动调用,不能手动调用它们。
- 子类实现了load和initialize的话,会隐式调用父类的load和initialize方法。
- load和initialize方法内部使用了锁,因此它们是线程安全的。
(2)不同点
- 调用顺序不同,以main函数为分界,
+load
方法在main函数之前执行,+initialize
在main函数之后执行。 - 子类中没有实现
+load
方法的话,子类不会调用父类的+load
方法;而子类如果没有实现+initialize
方法的话,也会自动调用父类的+initialize
方法。 +load
方法是在类被装在进来的时候就会调用,+initialize
在第一次给某个类发送消息时调用(比如实例化一个对象),并且只会调用一次,是懒加载模式,如果这个类一直没有使用,就不回调用到+initialize
方法。
4、使用场景
(1)+load
一般是用来交换方法Method Swizzle
,由于它是线程安全的,而且一定会调用且只会调用一次,通常在使用UrlRouter的时候注册类的时候也在+load
方法中注册。
(2)+initialize
方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作。