希望总结项目中的启动优化,详细学习iOS启动流程
1. 想要对App进行启动优化需要优先了解App的启动时间,和个个启动过程中的时间占比(主要讲解Pre-main之前的过程)
- Xcode 13.0 beta / iOS 15.0之前比较方便我们可以在 Xcode 中配置环境变量 DYLD_PRINT_STATISTICS 为 1(Edit Scheme → Run → Arguments → Environment Variables → +)。
- Xcode 13.0 beta / iOS 15.0之后这个方法失效了苹果推荐我们使用instrument工具进行监测
2. 了解了方法后我们可以通过打印看到我们每个过程消耗的时间,此处有几个关键指标
dylib loading (动态库加载)
rebase/binding (偏移修正/符号绑定)
rebase(偏移修正):任何一个app生成的二进制文件,在二进制文件内部所有的方法、函数调用,都有一个地址,这个地址是在当前二进制文件中的偏移地址。一旦在运行时刻(即运行到内存中),每次系统都会随机分配一个ASLR(Address Space Layout Randomization,地址空间布局随机化)地址值(是一个安全机制,会分配一个随机的数值,插入在二进制文件的开头),例如,二进制文件中有一个 test方法,偏移值是0x0001,而随机分配的ASLR是0x1f00,如果想访问test方法,其内存地址(即真实地址)变为 ASLR+偏移值 = 运行时确定的内存地址(即0x1f00+0x0001 = 0x1f01)
- Objc setup (Objc相关类的注册,selector唯一性检查)
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方法)
- initializer (初始化执行load方法,创建静态全局变量等)
3. 介绍几个概念
1.Mach-O文件
Apple出品的操作系统的可执行文件格式几乎都是mach-o,iOS当然也不例外。
mach-o可以大致的分为三部分:
- Header 头部,包含可以执行的CPU架构,比如x86,arm64
- Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
- Data,数据,包含load commands中需要的各个段(segment)的数据,每一个Segment都得大小是Page的整数倍。
2.Virtual Memory
虚拟内存是建立在物理内存和进程之间的中间层。在iOS上,当内存不足的时候,会尝试释放那些只读的Page,因为只读的Page在下次被访问的时候,可以再从磁盘读取。如果没有可用内存,会通知在后台的App(也就是在这个时候收到了memory warning),如果在这之后仍然没有可用内存,则会杀死在后台的App。
- 虚拟内存是在物理内存上建立的一个逻辑地址空间,它向上(应用)提供了一个连续的逻辑地址空间,向下隐藏了物理内存的细节。
- 虚拟内存使得逻辑地址可以没有实际的物理地址,也可以让多个逻辑地址对应到一个物理地址。
- 虚拟内存被划分为一个个大小相同的Page(64位系统上是16KB),提高管理和读写的效率。 Page又分为只读和读写的Page。
3.Page fault
在应用执行的时候,它被分配的逻辑地址空间都是可以访问的,当应用访问一个逻辑Page,而在对应的物理内存中并不存在的时候,这时候就发生了一次Page fault。当Page fault发生的时候,会中断当前的程序,在物理内存中寻找一个可用的Page,然后从磁盘中读取数据到物理内存,接着继续执行当前程序。
4.改善APP的启动
-在 dylib loading的过程中,会去装载app使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取,Apple官方建议尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们。简单的举个例子比如使用cocoapods管理的多个自定义的UI组件可以合并成一个自己的UIKIT,同时也建议动态库转静态库
-减少+load的方法使用,+load方法中尽量少做耗时操作,+load中代码延迟到 main 之后子线程处理或者首页显示之后;改为 initialize 中执行,针对 initialize 中处理需要注意的是分类 initialize 会覆盖主类 initialize 以及有子类后 initialize 执行多次的问题,需要使用 dispatch_once 来保证代码只执行一次;
-二进制重排要实现符号的重排,一是需要我们收集整个启动链路上的方法和函数等符号,二是需要生成对应的 order 文件来配置 ld 中的 Order File 属性。当工程在编译的时候,Xcode 会读取这个 order 文件,在链接过程中会根据这个文件中的符号顺序来生成对应的 MachO。一般业界中收集符号的方案有两种:
1.Hook objc_msgSend,只能拿到 OC 以及 swift @objc dynamic 的符号;
2.Clang 插桩,能完美拿到 OC、C/C++、Swift、Block 的符号;
第二种方法实现成本较高
采用第一种方法在编译完成后通过验证 LinkMap 文件中 #Symbols: 部分符号顺序是否和 order 文件中的符号顺序一致来确定是否配置成功即可