WWDC关于APP启动的建议

一些概念

Mach-O是运行时产生的可执行文件的文件类型
  • Image:

    • Executable:程序的主二进制文件
    • Dylib:动态库
    • Bundle:是一种特殊的Dylib,是不能进行链接的,只能在运行时用dlopen()函数打开。
  • Framework:Dylib+储存该Dylib需要的资源、头文件的目录结构

Mach-O Image File被分割成几个段
引用自WWDC2016
  • _TEXT:头文件,代码,只读常量
  • _DATA:所有可读写内容(全局变量、静态变量等等)
  • _LINKEDIT:储存关于如何加载程序的“元数据”

每个段都是page size的倍数,图中_TEXT占有三个page,arm64一个page size是16KB,其它的是4KB


虚拟内存把每一个进程地址映射到物理内存RAM中:
  • page错误
  • 多个进程中出现的相同的RAM page
  • 文件回溯page:mmap()、延迟读取(lazy reading)
  • Copy-On-Write(COW)
  • 脏page和干净page
  • Permissions:rwx


安全:

ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。据研究表明ASLR可以有效的降低缓冲区溢出攻击的成功率,如今Linux、FreeBSD、Windows等主流操作系统都已采用了该技术。

ASLR:
  • 地址空间随机布局
  • 镜像(Images)加载在随机的地址

代码签名

  • 每一个page拥有的内容
  • page内的哈希验证

exec()到main(),内核让你的App在随机的地址开始运行


什么是Dyld?
  • Dyld是动态加载器,内核加载的辅助程序
  • 程序从Dyld开始执行
  • Dyld运行在进程中
  • Dyld负责加载动态依赖库
  • Dyld拥有与App相同的权限



Main()函数之前的五个阶段

Dyld主导的五个阶段:
引用自WWDC2016
一、Load dylibs:

动态映射所有的动态依赖库。

  1. 解析动态依赖库(dylib)的列表
  2. 找到所有需要的mach-o(dylib)文件
  3. 打开并读取每一个找到的文件
  4. 验证这些文件是不是mach-o文件
  5. 找到它的编码签名,在内核里对它进行注册
  6. 给每一个分段调用映射

现在,所有App指向的动态依赖库都被递归加载,App通常需要加载100-400个动态库,大多数是OS(操作系统)的动态库

二、Rebase

遍历所有内部数据指针为他们添加一个滑动值。这些指针的位置都被编码在__LINKEDIT段里。

  1. 调整所有镜像内的指针,添加一个slide偏差值。
Slide = actual_address - preferred_address
  1. 出于安全考虑,引入了 ASLR,全称 Address Space Layout Randomization

大概意思就是镜像(dylib)会加载在随机的地址上,和actual_address会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。

  1. Rebase+Bind+ObjC大多数时间在做修复:
    • 代码签名意味着命令不能被修改
    • 代码不能被加载到任何地址上,而且永远不能被修改
    • 所有的修复都发生在__DATA数据段
三、Bind

遍历查询符号表,设置指向镜像外部的指针。

  1. 所有在其它动态库引用的东西都会符号化
  2. Dyld需要找到所有符号名
  3. 会比Rebasing进行更多的计算
四、ObjC

通知 runtime 准备镜像、OC类的注册、偏移ivar的地址、加载Category

  1. runtime要维护一张表,包含所有类名(Class Name)及其映射的类(Class)
  2. 完成所有OC类的定义注册
  3. 运行时改变所有ivar的偏移量
  4. 接下来在ObjC阶段可以定义分类(Category)
  5. 最后让Selector都是唯一的
五、Initializers
  1. dyld开始调用C++静态构造函数,初始化器用来初始化那些抽象DATA

  2. 调用所有类的 +load 方法,顺序:父类->子类->Category

  3. 每个Initializers按照从下向上的顺序执行。

    为什么从下向上?

    因为当Initializers运行时,可能会调用一些dylib,我们需要确保那些dylib已经准备好被调用,所以从下开始运行Initializers,一直往上到应用类,可以很安全的调用依赖的内容。

  4. 最后Dyld调用main()函数

小结:Dyld是一个辅助程序
  1. 加载所有的依赖库
  2. 修复所有DATA page中的指针
  3. 运行所有的初始化器(Initializers)
  4. 跳转到主函数

优化启动时间的实践部分

  • 启动时间如果在400ms(0.4s)以内会让用户觉得启动快
  • 启动时间千万不要超过20s,否则OS将会认为你的APP进入死循环,杀死APP
App启动都做了什么?

在main函数之前的五个阶段之后,还要调用:

  1. main()

  2. UIApplicationMain() : 加载framework的初始化器,加载nibs

  3. applicationWillFinishLaunching

以上8个步骤都算在这400ms内。


热启动和冷启动
  • 热启动:App及其数据已经在内存(磁盘)中
  • 冷启动:App不在内核的缓存中


优化方案:

一、Load Dylib阶段
  1. 合并已有的动态库(包括framework),限制动态库(包括framework)个数效果非常明显
  2. 使用静态库代替

  3. 可以使用延迟加载,也就是使用dlopen()函数,但是dlopen()会带来细微的性能和正确性的问题。


    优化前的链接库个数

    优化前的启动时间

原有26个framework合并成2个后,由240ms变为21ms。

优化后的链接库个数

优化后的启动时间
二、Rebase和Bind阶段
  1. 减少OC元数据
    • 减少OC类的数量(不鼓励使用很多很小的类,只有一两个方法的那种)
    • 减少selector的数量
    • 减少Category的数量
  2. 减少C++虚拟函数,虚拟函数创建被称作 V表格,和OC元数据相同

  3. 避免让机器生成过多的代码,机器生成的指针非常耗内存
    • 使用偏移量代替指针
    • 标记为只读
三、ObjC阶段

这个阶段的优化工作已经在Rebase和Bind阶段做完了

四、Initializers阶段

有两种Initializers:显式和隐式

显式:
  1. +load
方案:使用 +initiailize 代替


  1. C/C++的 __attribute__((constructor))

方案:使用site initializers

  • 使用dispatch_once()
  • 使用pthread_once()
  • 使用std::once()

隐式:大部分是C++的全局变量带来的非默认初始化器
  1. 使用site initializers
  2. 只设置简单的值
  3. 不要在初始化器中调用dlopen()
  4. 不要在初始化器中创建线程


参考

WWDC2016-406