一、应用启动流程
1、整体过程
(1)解析Info.plist
- 加载相关信息,例如如闪屏
- 沙箱建立、权限检查
(2)Mach-O(可执行文件)加载
- 如果是胖二进制文件(为了保持向下兼容,且支持旧有设备及旧有指令集),寻找合适当前CPU类别的部分
- 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
- 定位内部、外部指针引用,例如字符串、函数等
- 执行声明为__attribute__((constructor))的C函数
- 加载类扩展(Category)中的方法
- C++静态对象加载、调用ObjC的 +load 函数
(3)程序执行
- 调用main()
- 调用UIApplicationMain()
- 调用applicationWillFinishLaunching
2、阶段:
分为两个阶段,pre-main阶段和main()阶段。程序启动到main函数执行前是pre-main阶段;在执行main函数后,调用AppDelegate中的-application:didFinishLaunchingWithOptions:
方法完成初始化,并展示首页,这是main()阶段,或者叫做main()之后阶段。
(1)pre-main阶段:
- 加载应用的可执行文件。
- 加载动态链接库加载器dyld(dynamic loader)。
- dyld递归加载应用所有依赖的dylib(dynamic library 动态链接库)。
- 进行
rebase
指针调整和bind
符号绑定。 -
ObjC
的runtime
初始化(ObjC setup):ObjC
相关Class
的注册、category
注册、selector
唯一性检查等。 -
初始化(Initializers):执行
+load()
方法、用attribute((constructor))
修饰的函数的调用、创建C++
静态全局变量等。
(2)main()阶段:
- dyld调用main()
- 调用UIApplicationMain()
- 调用applicationWillFinishLaunching
- 调用didFinishLaunchingWithOptions
二、获取启动流程的时间消耗
1、pre-main阶段
对于pre-main阶段,Apple提供了一种测量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量DYLD_PRINT_STATISTICS 设为1 。之后控制台会输出类似内容,我们可以清晰的看到每个耗时:
从上面可以看出时间区域主要分为下面几个部分:
- dylib loading time
- 动态库载入过程,会去装载app使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取。
- dyld (the dynamic link editor)动态链接器,是一个专门用来加载动态链接库的库,它是开源的。在 xnu 内核为程序启动做好准备后,执行由内核态切换到用户态,由dyld完成后面的加载工作,dyld的主要是初始化运行环境,开启缓存策略,加载程序依赖的动态库(其中也包含我们的可执行文件),并对这些库进行链接(主要是rebaseing和binding),最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。
- rebase/binding time
ASLR(Address Space Layout Randomization),地址空间布局随机化。在ASLR技术出现之前,程序都是在固定的地址加载的,这样hacker可以知道程序里面某个函数的具体地址,植入某些恶意代码,修改函数的地址等,带来了很多的危险性。ASLR就是为了解决这个的,程序每次启动后地址都会随机变化,这样程序里所有的代码地址都需要需要重新对进行计算修复才能正常访问。rebasing这一步主要就是调整镜像内部指针的指向。
Binding:将指针指向镜像外部的内容。
- ObjC setup time
- dyld调用的
objc_init
方法,这个是runtime的初始化方法,在这个方法里面主要的操作就是加载类(对需要的class和category进行注册); - objc_init方法通过内部的_dyld_objc_notify_register向dyld注册了一个通知事件,当有新的image(程序中对应实例可简称为image,如程序可执行文件macho,Framework,bundle等)加载到内存的时候,就会触发
load_images
方法,这个方法里面就是加载对应image里面的类,并调用load
方法(在下一阶段initializer)。 - 如果有继承的类,那么会先调用父类的
load
方法,然后调用子类的,但是在load
里面不能调用[super load]
。最后才是调用category的load
方法。总之,所有的load
都会被调用到(注意:子类的initialize方法会覆盖父类,不同于load方法)。
- dyld调用的
- initializer time
承接上一过程进行初始化(load)。如果我们代码里面使用了clang的__attribute__((constructor))
构造方法,这里会调用到。
2、main()阶段
测量main()函数开始执行到didFinishLaunchingWithOptions执行结束的时间,简单的方法:直接插入代码。(也可以使用其他工具)
- main函数里
- 到主UI框架的.m文件用extern声明全局变量StartTime
- 在viewDidAppear函数里,再获取一下当前时间,与StartTime的差值即是main()阶段运行耗时。
三、改善APP的启动
建议应用的启动时间控制在400ms之下,并且在20s内启动,否则系统会kill app。优化APP的启动时间,需要就是分别优化pre-main和main的时间。
1、改善启动时pre-main阶段
(1)加载 Dylib
(2)Rebase/Binding
大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会注册所有声明过的ObjC类,将分类插入到类的方法列表里,再检查每个selector的唯一性。
在这一步倒没什么优化可做的,Rebase/Bind阶段优化好了,这一步的耗时也会减少。
(4)Initializers
到了这一阶段,dyld开始运行程序的初始化函数,调用每个Objc类和分类的+load方法,调用C/C++ 中的构造器函数(用attribute((constructor))修饰的函数),和创建非基本类型的C++静态全局变量。Initializers阶段执行完后,dyld开始调用main()函数。
在这一步,我们可以做的优化有:
-
- 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize
- 减少构造器函数个数,在构造器函数里少做些事情
- 减少C++静态全局变量的个数
2、main()阶段的优化
(1)核心点:didFinishLaunchingWithOptions方法
这一阶段的优化主要是减少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里我们经常会进行:
- 创建应用的window,指定其rootViewController,调用window的makeKeyAndVisible方法让其可见;
- 由于业务需要,我们会初始化各个三方库;
- 设置系统UI风格;
- 检查是否需要显示引导页、是否需要登录、是否有新版本等;
由于历史原因,这里的代码容易变得比较庞大,启动耗时难以控制。
(2)优化点:
满足业务需要的前提下,didFinishLaunchingWithOptions在主线程里做的事情越少越好。在这一步,我们可以做的优化有:
- 梳理各个二方/三方库,把可以延迟加载的库做延迟加载处理,比如放到首页控制器的viewDidAppear方法里。
- 梳理业务逻辑,把可以延迟执行的逻辑做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
- 避免复杂/多余的计算。
- 避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理。
- 首页控制器用纯代码方式来构建。
参考链接: