App启动分成两部分
- pre-main 阶段的定义为 APP 开始启动到系统调用 main 函数这一段时间
- main 阶段则代表从main函数入口到主 UI 框架的 viewDidAppear
Pre-main
dyld:dynamic loader,它的作用是加载一个进程所需要的image,dyld是开源的
Load Dylibs
dyld加载App的可执行文件,App内嵌的库,系统动态库。每个库,就是Mach-o文件,其头部都包含依赖库信息,dyld会递归加载这些库。
- 系统库会有缓存,dyld共享库缓存,加载很快
- 内嵌的库加载较慢
加载过程
- 分析所依赖的动态库
- 找到动态库的mach-o文件
- 打开文件
- 验证文件
- 在系统核心注册文件签名
- 对动态库的每一个segment调用mmap()
优化:
- 减少非系统库的依赖
- 使用静态库而不是动态库
- 合并多个非系统动态库为一个动态库
Rebase && Binding
ASLR的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。
- Rebase 修正内部(指向当前mach-o文件)的指针指向。是因为刚刚提到的ASLR使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在__LINKEDIT中
-
Bind 修正外部指针指向。外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的
优化
- 减少Objc类数量, 减少selector数量,把未使用的类和函数都可以删掉。跟单职责有点冲突?
- 减少C++虚函数数量
- 转而使用swift stuct(其实本质上就是为了减少符号的数量,使用swift语言来开发?)
OBJC Runtime Setup
- 读取二进制文件的 DATA 段内容,找到与 objc 相关的信息
- 注册 Objc 类,ObjC Runtime 需要维护一张映射类名与类的全局表。当加载一个 dylib 时,其定义的所有的类都需要被注册到这个全局表中;
- 读取 protocol 以及 category 的信息,把category的定义插入方法列表 (category registration),
- 确保 selector 的唯一性
Initializers
以上三步属于静态调整,都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和栈中写入内容。 工作主要有:
- Objc的+load()函数
- C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
- 非基本类型的C++静态全局变量的创建。(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度
优化
- 使用 +initialize 来替代 +load
- 不要使用 atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。
- 比如使用
dispatch_once()
,pthread_once()
或std::once()
。也就是在第一次使用时才初始化,推迟了一部分工作耗时。 - 也尽量不要用到C++的静态对象。
- 比如使用
Pre-main 总结
-
pre-main阶段耗时的影响因素:
- 动态库加载越多,启动越慢。
- ObjC类越多,函数越多,启动越慢。
- 可执行文件越大启动越慢。
- C的constructor函数越多,启动越慢。
- C++静态对象越多,启动越慢。
- ObjC的+load越多,启动越慢。
-
整体上pre-main阶段的优化有:
- 减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;
- 检查下 framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
- 合并或者删减一些OC类和函数;关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类(也可以用根据linkmap文件来分析,但是准确度不算很高);有一个叫做FUI的开源项目能很好的分析出不再使用的类,准确率非常高,唯一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++的类模板。
- 删减一些无用的静态变量,
- 删减没有被调用到或者已经废弃的方法
- 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)
- 类和方法名不要太长:iOS每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的;因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来;
- 用dispatch_once()代替所有的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load函数;
- 在设计师可接受的范围内压缩图片的大小,会有意外收获。压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了,比较靠谱的压缩算法是TinyPNG。
Main
总体原则无非就是减少启动的时候的步骤,以及每一步骤的时间消耗。
main阶段的优化大致有如下几个点:
- 减少启动初始化的流程,能懒加载的就懒加载,能放后台初始化的就放后台,能够延时初始化的就延时,不要卡主线程的启动时间,已经下线的业务直接删掉;
- 优化代码逻辑,去除一些非必要的逻辑和代码,减少每个流程所消耗的时间;
- 启动阶段使用多线程来进行初始化,把CPU的性能尽量发挥出来;
- 使用纯代码而不是xib或者storyboard来进行UI框架的搭建,尤其是主UI框架比如TabBarController这种,尽量避免使用xib和storyboard,因为xib和storyboard也还是要解析成代码来渲染页面,多了一些步骤;
- 主UI框架tabBarController的viewDidLoad函数里,去掉一些不必要的函数调用
其中遇到几个坑:
- 并不是什么任务都适合放子线程,有些任务在主线程大概10ms,放到子线程需要几百ms,因为某些任务内部可能会用到UIKit的api,又或者某些操作是需要提交到主线程去执行的,关键是要搞清楚这个任务里边究竟做了啥,有些SDK并不见得适合放到子线程去初始化,需要具体情况具体去测试和分析。
实际优化效果
由于只是去掉了几个静态库,而且本来pre-main阶段的耗时就不长,基本在200ms-500ms左右,所以pre-main阶段优化前后效果并不明显,有时候还没有前后测试的误差大。。。
main的阶段的优化效果还是很明显的:
- iPhone5C iOS10.3.3系统优化前main阶段的时间消耗为4秒左右,优化后基本在1.8秒内;
- iPhone7 iOS10.3.3系统优化前main阶段的时间消耗为1.1秒左右,优化后基本在600ms内;
- iPhoneX iOS11.3.1系统优化前main阶段的时间消耗基本在1.5秒以上,优化后在1秒内;
可以看到,同样arm64架构的机器,main阶段是iPhone7比iPhoneX更快,说明就操作系统来说,iOS11.3要比iOS10.3慢不少;