iOS main()之前都发生了什么

这篇文章首发在公司微信技术公众号:京东金融技术说

「摘要」每个iOS app从点击启动到首页面加载渲染完成,虽然时间比较短暂,但系统进行了不少重要的操作,比如会加载100-400个支撑app后续运行的dylib。本文从Dylib、Mach-O等基本运行时文件说起,介绍了iOS 的Virtual Memory,以及从exec()到main()之间经历的主要阶段:Dylibs Loading、Rebasing、Binding、ObjC Runtime、Initializer。然后从理论升级到实践,针对启动中的各个环节分析如何更进一步提升app的启动速度。

一、Mach-O

相关术语说明(文件类型)

Executable—iOS 应用程序的主要二进制文件

Dylib—Dynamic library,动态库 (其他平台中叫DSO 或者 DLL)

Bundle—可以看做是不能链接的Dylib,只能使用dlopen()来加载,就像插件一样

Image— 泛指 Executable,Dylib或者Bundle

Framework—包含Dylib、图片等资源、头文件(.h)的特定结构的目录文件

1、Mach-O Image

Image 被分割成多个 segment。segment一般用大写字母来命名,其大小一般是 page 大小的整数倍(arm64环境下1page是16KB,其它环境下是4KB)。如下图,一个Image通常由三个segment组成,分别是__TEXT(3page)、__DATA(1page)、__LINKEDIT(1page)。

iOS main()之前都发生了什么_第1张图片

Section 是Segment所包含的一部分,一般用小写字母命名。Section的大小没有是page整数倍的要求,但它是不可被覆盖的,同时它也是被编译器忽略的。如下图,__TEXT是文件的开始,包含了Machheader,主要有对应硬件环境信息的说明,只读的常量(如C字符串)。__DATA是可读亦可写的,主要包含一些全局变量,静态变量。__LINKEDIT并不包含全局变量的函数/方法(function),而是包含函数/方法(function)的一些信息,比如其名字,地址等等。

iOS main()之前都发生了什么_第2张图片

2、Mach-O universal file(通常称为胖二进制文件)

由于硬件在快速的升级换代,之前是32位的机器,现在已经有好多的64位机器。并且,每一代的iPhone,其CPU架构都不完全相同,有i386,armv7,armv7s,arm64。为了让同一份代码可以部署到不同的环境下,就需要一种通用的可执行文件了—Mach-O universal file。如下图,将armv7s和arm64下的Mac-O文件合并,形成一个新的Mach-O universal file,其包含了一个Fat Header。Fat Header中包含了所支持的架构列表以及其在文件中的偏移量(offsets)。

iOS main()之前都发生了什么_第3张图片

二、Virtual Memory

Virtual Memory 是一个中间层,用于将每个进程(process)的逻辑地址空间映射到物理RAM上(以page的粒度)。VM有如下的特点:

某些逻辑地址没有映射到具体物理RAM上,内核访问该地址时,会出现Pagefault

多个process可以映射到相同的一个page 上

可以实现对文件的延迟读取

对于共享的page可以进行 Copy-On-Write

Copy-On-Write 会导致Dirtypage,所以需要pageclean

RAM的权限(rwx)与映射权限的联系

1、Mach-O ImageLoading

结合上面的Dylib和VM的基本知识,Mach-O Image 的加载过程可通过下图来展示出来:

iOS main()之前都发生了什么_第4张图片

两个重要的安全因素对Image Loading的影响:

一个是 ASLR:(address space layout randomization),为了防止Image 每次都被加载到同一个物理RAM上而被恶意利用,Image每次分配到的地址偏移量是随机的。

另一个是 code sign:这个使用XCode编译打包过的人应该非常熟悉了,和编译期不同的是,每一个page的Mach-O都有自己的签名,这些签名信息保存在__LINKEDIT中。

三、从exex() 到 main()

exec()是一个系统级别的调用,当点击appicon或者不同app间切换时,会触发它。对于该app,exec()主要进行如下的操作:

将app映射到一个随机的地址空间

app的起始地址是随机的

将起始地址和0x000000之间的地址空间,对该app标记为不可操作(不可读,不可写,不可执行)

捕获空指针的使用

捕获指针缺失的错误

iOS main()之前都发生了什么_第5张图片

1、加载Dylibs

接下来,Dyld(dynamicloader)会开始加载app中的dylib,主要步骤如下:

从胖二进制文件中的header中解析app所依赖的dylibs列表

找到该环境下所需的mach-O 文件

打开每一个dylib,读取其头部,进行验证,看是否是mach-O格式,然后找到代码签名,并将代码签名注册到内核

调用mmap(),对所有的segment进行映射

现在,所有的一级dylib都加载了,但是有的dylib依赖于其他dylib,甚至同一dylib被多个dylib依赖。所以需要进行递归的加载,一直到所有的都加载完成

通常一个app会加载100-400个dylib,这其中大部分是操作系统的,系统本身已经对这些dylib的加载进行了优化

上述过程如下图:

2、Rebasing 和 Binding

每个dylib分配到的地址空间也是随机的,也就是其起始地址会不断的变化,或者说是滑动(slide)。

Rebasing就是在发生变化时,对内部的指针按照新的偏移量进行校正。而binding是对指向外部的指针进行校正。校正后的地址信息都是保存在__LINKEDIT中。

iOS main()之前都发生了什么_第6张图片
iOS main()之前都发生了什么_第7张图片

可以使用 dyldinfo 命令来查看任意 dylib 的Rebasing和Binding信息:

iOS main()之前都发生了什么_第8张图片

2、通知 ObjC Runtime

大多数的ObjC设置都通过Rebasing和Binding完成了,比如注册ObjC class 声明,将Category中声明的方法插入到方法列表中。之后,Runtime启动并开始初始化操作

3、Initializer

如果是C++,静态对象的initializer开始执行。对于ObjC,+load 方法被调用。其她的+方法也开始被调用。由于+方法之间会存在继承调用关系,所以,整个+方法的被调用顺序是由下向上的。

接下来,app中的main()方法会被调用,main()被调用前经历的TimeLine:

四、从理论到实践-如何提升启动时间?

1、怎样的速度算快

启动速度应当比闪屏动画速度快,不同的硬件设备、系统环境下,启动速度都不相同,400ms是一个比较合理的目标,启动时间不能超过20 秒,超过了会被系统Kill掉

需要在低性能设备上做测试。

2、回顾一下整个启动堆栈

Parse  images

Map  images

Rebase  images

Bind  images

Run  imageinitializers

Call  main()

Call  UIApplicationMain()

CallapplicationWillFinishLaunching

Call applicationdidFinishLaunchingWithOptions(iOSer 写应用程序的入口)

3、冷启动、热启动,检测启动时间

热启动是指app启动之前,app执行文件和相关的数据已经在缓存中。冷启动则相反,内核缓存中没有任何数据,所以对于冷启动来说,时间更为重要。冷启动的使用场景是,更新系统后第一次打开app,或者很长时间没有使用过app,再次打开。

之前,检测main()之前的时间消耗是非常困难的,在最新的iOS10系统,嵌入了新功能,可以方便的测试启动时间:

iOS main()之前都发生了什么_第9张图片


iOS main()之前都发生了什么_第10张图片

4、dylib 加载阶段

尽量减少dylib 的数量,可以合并多个dylib,或者将代码直接放在executable中。

使用dlopen来延迟加载,虽然使用dlopen会导致额外的开销,甚至大于在启动时加载的,但它是延迟的。(注:近一年内Hot patch非常火热,由于Hot patch本身的机制和安全问题,导致苹果开始封杀,dlopen就被加入了禁用名单了)

从架构的角度,把app中的一些共用功能以dylib的形式模块化,可以降低耦合,提升开发效率。但是过多的话,会影响启动时间。

5、Rebasing和Binding阶段

Rebasing和Binding阶段,会产生IO和一些计算消耗,可以通过如下方案优化:

减少__DATA 中的指针数量可以减少处理时间

减少Objective-C的元数据,如Classes,selectors,categores

减少C++中的虚方法

使用swift 的 struct

6、Initializer阶段

ObjC +load方法替换成+initialize 形式的:C/C++中的__attribute__((constructor)) 换成 dispatch_once(),pthread_once(),std:once()

不要在initialize中使用 dlopen

不要在initialize中创建线程

使用swift


整理自WWDC 2016 - Session 406

你可能感兴趣的:(iOS main()之前都发生了什么)