启动过程
进程创建 -> 第一个 CATransaction::Commit
Mach-O
分主二进制和动态库
- Header
- Load Commands: 存储 Mach-O 的布局信息和依赖动态库的信息
- Data: 包含代码和数据,每个 Data 包含多个 Segment,每个 Segment 包含多个 Section
标准的 Segment
- TEXT: 代码段,存储函数二进制代码__text,常量字符串__cstring,OC的类方法名等信息
- DATA: 数据段,存储 OC 的字符串__cstring,运行时的元数据
- LINKEDIT: 启动 App 需要的信息,bind & rebase 地址,代码签名、符号表
dyld
App 启动辅助程序
- dyld2: iOS3-iOS12,dyld shared cache, 将 UIKit 合成一个大文件,提升加载性能的缓存文件
- dyld3: iOS13+, 使用启动闭包,包含启动所需的缓存信息,提升启动速度
虚拟内存
- 虚拟内存是在物理内存的基础上建立的一层逻辑地址,提供连续访问地址空间。虚拟内存跟物理内存按 Page 为单位映射,不是一一映射的。
- iPhone6S 后物理内存Page大小是 16k
mmap
一种将文件映射到虚拟内存空间的技术,通过操作内存一样读取文件。读取虚拟内存时,文件在物理内存中不存在,触发一个事件:File Backed Page In,Page In 后会进行 zero fill
Page In 过程
- MMU 找到空闲的物理内存页
- 触发 IO,数据读取到物理内存
- 如果是 TEXT段的页,解密 (代码混淆,iOS13 进行优化,Page In 不需要解密)
- 解密后的页进行验签
二进制重排
启动具有局部特征,函数在二进制中的位置是随机分布的,所以 Page In 读入数据利用率不高,因此把启动阶段的函数排列到二进制的连续空间,这样可以降低 Page In 的次数,从而优化启动时间。 链接器的 ld 有一个order_file支持按照符号的方式进行二进制重排。
IPA 构建过程
- 源文件(m,swift.c)编译输出.o文件
- 目前文件跟静态库/动态库一起链接 mach-O
- mach-O裁剪
- 编译资源文件(asset, storyboard)
- mach-O,资源文件打包.app
- app 签名防篡改
编译
- 编译前端(clang,switc):语法分析,词法分析生成IR
- 编译后端(LLVM)将IR生成机器码
编译优化
下线无用代码控制代码数量, LLVM 插桩的方式标记无用代码
链接优化
- 通过ld的-rename_section将 TEXT 中的诸如 sctring 移动到其他段,节约解密耗时。
dyld3 启动过程
- Before dyld
- 用户点击 icon
- 发送系统消息 execve 到内核
- 把主二进制 mmap 进来,找到 Load Commands 中的 LC_LOAD_DYLINKER,找到 dyld 路径
- mmap dyld到虚拟内存,找到 dyld入口函数 dyld_start,将 PC 寄存器设置为 dyld_start
- dyld
- 创建启动闭包,启动闭包存储到沙盒
- 启动闭包包含:
- dependends: 动态库依赖列表
- fixup: bind & rebase 地址
- initial-order: 初始化调用顺序
- optimizeObjc: objc元数据 (解析很慢)
- 其他: main entry, uuid
- fixup
- 加载动态库时先把每个动态库 mmap到虚拟内存,然后会对每个 mach-o fixup, bind 修复内部指针,加上随机偏移,rebase 修复外部指针,引用外部函数运行时才知道真正的地址。
- LibSystem Initialize
- 初始化 libdispatch
- 初始化 runtime,注册 SEL, category
- Load & Static Initialize
- 调用顺序不确定,跟链接文件顺序有关
- static initialize 产生条件
- __attribute ((constructor))
- static class object
- static object in global namespace
- main函数
- 初始化 UIApplication
- 启动 Runloop
- AppLifeCycle
- willFinishLaunch
- didFinishLaunch
- didFinishLaunchNotification
- First Frame Render
- Layout
- Display
- Prepare: 图片解码
- Commit:打包 Render Tree 通过 XPC 提交给 Render Server
启动过程总结
- 点击图标,创建进程
- mmap 主二进制,找到 dyld 的路径
- mmap dyld,把入口地址设为
_dyld_start - 重启手机/更新/下载 App 的第一次启动,会创建启动闭包
- 把没有加载的动态库 mmap 进来,动态库的数量会影响这个阶段
- 对每个二进制做 bind 和 rebase,主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据
- 初始化 objc 的 runtime,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category
- +load 和静态初始化被调用,除了方法本身耗时,这里还会引起大量 Page In
- 初始化
UIApplication,启动 Main Runloop - 执行
will/didFinishLaunch,这里主要是业务代码耗时 - Layout,
viewDidLoad和Layoutsubviews会在这里调用,Autolayout太多会影响这部分时间 - Display,
drawRect会调用 - Prepare,图片解码发生在这一步
- Commit,首帧渲染数据打包发给 RenderServer,启动结束
dyld2 & dyld3
dyld2 缺少闭包,每次启动需要:
- 解析动态库依赖关系
- 解析 LINTEDIT,找到 bind & rebase 地址,找到 bind 符号地址
- 注册 objc Class/Method等元数据