App 的启动流程分为两个阶段:pre-main 和 main
pre-main 阶段
- 读取 App 的可执行文件(Mach-O 文件),从里面获得 dyld 的路径
- 加载 dyld
dyld 加载动态库
加载动态库
dyld 从主执行文件的 header 中获取到需要加载的所依赖动态库列表,然后它需要找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,最终递归加载所有动态库
rebase 和 binding
- rebase 修正镜像内部的指针
- binding 修正镜像外部的指针
objc setup
- 注册 objc 类(class registration)
- 将分类的方法插到类的方法列表里(category registration)
- 确保 selector 的唯一性(selector uniquing)
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
优化方法
见脑图