启动

App 的启动流程分为两个阶段:pre-main 和 main

脑图

pre-main 阶段

  1. 读取 App 的可执行文件(Mach-O 文件),从里面获得 dyld 的路径
  2. 加载 dyld
  3. dyld 加载动态库

    1. 加载动态库

      dyld 从主执行文件的 header 中获取到需要加载的所依赖动态库列表,然后它需要找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,最终递归加载所有动态库

    2. rebase 和 binding

      • rebase 修正镜像内部的指针
      • binding 修正镜像外部的指针
    3. objc setup

      • 注册 objc 类(class registration)
      • 将分类的方法插到类的方法列表里(category registration)
      • 确保 selector 的唯一性(selector uniquing)
    4. initializer

      • 调用 objc 类和分类的 load 方法
      • C++ 的构造函数属性函数
      • 非基本类型的 C++ 静态全局变量的创建(即类 or 结构体)

以上整个过程由 dyld 主导,结束后,dyld 调用真正的 main 函数

小问题:那什么是 Mach-O 呢

Mach-O 是 OSX 和 iOS 系统中可执行文件的格式,主要包括以下几种类型:

  • Executable:应用的主要二进制
  • Dylib:动态链接库
  • Bundle:不能被链接,只能在运行时使用 dlopen 加载
  • Image:镜像文件,包含 Executable、Dylib 和 Bundle
  • Framework:包含 Dylib、资源文件和头文件的文件夹

小问题:dyld 是什么?

dyld(dynamic loader),是苹果的动态链接器,用于加载动态链接库

小问题:为什么需要 rebase 和 binding

iOS 采用 ASLR 技术来保证 App 的安全。

ASLR(Address Space Layout Randomization):地址空间布局随机化,是操作系统中使用的一种安全技术。可执行文件的地址空间有一个起始地址,而 ASLR 使得这个起始地址在 App 每次启动后是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址

一个 Mach-O 文件内部有很多符号,有指向当前 Mach-O 的,也有指向其他 dylib 的,由于 ASLR 的存在,这些符号的地址都是不对的。

比如在运行时,代码如何准确的找到 printf 函数的地址或者 NSObject 类的地址呢?

rebase 的作用把 Mach-O 文件读入内存,然后在当前 Mach-O 的起始地址添加一个偏移量,以此修正当前可执行文件内部符号的地址,解决可执行文件内部的符号引用。注意 rebase 的意思就是变基,顾名思义,修改的是起始地址

binding 的作用是使用字符串匹配的方式去查找符号表,以修正可执行文件外部符号的地址,解决可执行文件外部的符号引用。这个过程相对于 rebase 会略慢。比如当前的 Mach-O 文件没有 NSObject 这个符号,它是属于 Foundation 框架的,那么 binding 的作用就是将 NSObject 这个符号与其真正的地址进行绑定

小问题:什么是确保 selector 的唯一性

分类有可能有和本类同名的方法,对于普通方法,会优先调用分类的方法;如果不同的分类实现了相同的方法,则编译顺序靠后的会被调用

确保 selector 唯一性就是找到同名方法的真正调用地址

小问题:什么是热启动和冷启动

  • 冷启动是指 app 进程不存在的情况下启动 App,需要创建和初始化进程
  • 热启动是指 app 进程就驻在内存中,进程状态可能是激活的,可能是睡着的,系统将该进程激活,并放到前台。也就是没有了创建和初始化的过程,只有状态的切换

main 阶段

dyld 调用 main -> 调用 UIApplicationMain -> 最终调用 didFinishLaunchingWithOptions

优化方法

脑图

参考

  1. pre-main 阶段
  2. main 阶段
  3. 优化方法
  4. 参考