To The West - Launch Progress

214 阅读4分钟

启动过程

进程创建 -> 第一个 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

启动过程总结

image.png

  • 点击图标,创建进程
  • 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等元数据

Reference

juejin.cn/post/688774…