iOS 艾欧艾斯Kingsly的iOS开发进阶专题iOS学习

优化应用的启动时间(理论篇)

2016-08-11  本文已影响511人  StanOz

这是 WWDC 2016 Session 406 理论部分的笔记,内容包含着了 Mach-O,虚拟内存的一点点知识,不过主要还是关注在 main() 函数之前做了什么。

Mach-O

Mach-O 是一种运行时可执行二进制文件类型。除了在应用中常见的可执行文件之外,还有 dylib(动态库),bundle(特殊的 dylib,只能在运行时通过 dlopen() 函数装载)等等,都是 Mach-O 文件,也被称作 Image。这些文件运行在那些基于 Mach 内核的操作系统上,比如 macOS 和 iOS 等等。

Mach-O 文件的结构

Mach-O_Format.png

Mach-O 被分为不同的 segment,每个 segment 的大小都是页面大小(page size)的倍数(arm64 环境下 page size = 16KB,其它为 4K)。常见的有 __TEXT, __DATA, __LINKEDIT。通过 otool -tV -d 可以读取对象文件的 __TEXT 段和 __DATA 段的内容:

  1. __TEXT segment 包含 Mach 头文件、代码以及只读常量;
  2. __DATA segment 包含有可读的内容,如全局变量、静态变量等等;
  3. __LINKEDIT segment 包含有如何装载程序的元数据(meta data)。

Universal Binary

Univseral_Files.png

Universal Binary 最早在 WWDC 2005 上被提出,目的是帮助 OS X 上的应用从基于 PowerPC 架构到 Intel 架构的转变。即可执行文件中包含着多种指令集,不同的系统可以根据 Mach-O 上的 Fat Header 上的信息选择执行相应的指令,副作用是使得可执行文件的体积增大。

比如将 Build Setting -> Valid Architectures 中的 armv7 和 armv7s 删除,仅剩 arm64,那么导出的可执行文件的体积会小很多(除非应用仅支持64位处理器,否则不要这么做)。

Size_Cmp_Diff_Arch.png

虚拟内存

虚拟内存是一种将进程虚拟地址空间映射到物理地址的一种机制。使用虚拟地址的好处是使得程序空间独立。一般的操作系统在实现虚拟内存的时候,都会将内存空间划分为页(page),通过页表(page table)去管理虚拟页和物理页之间的映射关系和 page-in & page-out 。

ASLR

地址空间布局随机化(Address Space Layout Randomization),即将可执行文件、动态库等文件随机地装载到内存的某个地址中防止缓冲区溢出攻击。

Code Signing

每一个页的内容都被加密散列,散列值存放在 __LINKEDIT segment 中。在 page-in 的时候会检验内容的正确性,这意味着程序的指令不会被修改。

从 exec() 到 main()

在执行 main() 函数之前,大概经过了这些步骤:

Loading Dylibs :

动态库(或者叫共享库,通常是 .so .dylib 作为后缀)是一种可以在程序在运行时装载的目标文件。使用动态库可以有效减少代码的体积,提高代码的复用率。下面是 iOS 在运行前加载动态库的过程:

  1. 解析程序所依赖的 dylibs;
  2. 找到所需的 Mach-O 文件;
  3. 打开并读取文件;
  4. 校验 Mach-O 文件;
  5. 向内核注册代码签名;
  6. 对每个 segment 调用 mmap(),将目标文件映射到内存中。

上面的这个过程是递归进行的,因为一个动态库可能还依赖着另一个动态库。

Rebasing

Rebasing 的工作是改变 dylibs 或者 bundles 的基地址。基地址是 image(dylib 或者 bundle)在被装载时优先选择的地址,被编码到 __LINKEDIT 中,默认为零。在运行时,如果基地址范围被占用了,那么 dyld 会将这个 image 装载到一个新的地址空间去(内容来源于 rebase 的 manual,在 terminal 里敲入 man rebase 就能看到)。

Binding

不同于 rebasing(修正 dylib 内部每个指向 image 内部的地址),binding 是要修正所有指向其它 dylib 指针的值。比如说在程序中调用 malloc 函数,dyld 需要在共享库中找到 malloc 这个符号对应的子程序的地址然后修改调用程序中的指针。

这里有个命令可以查看可执行文件中的 dyld 信息:
xcrun dyldinfo -rebase -bind -lazy_bind YourExecutableFile

Notify ObjC Runtime

在 rebasing 和 binding 结束之后,ObjC runtime 初始化,接着通过 class_createInstance()注册类到 runtime 中。类的成员变量的偏移量随之更新,category 上的方法也被插入到方法列表中。

Initializers

之后调用 ObjC 中类的 +load 方法。视频中 Nick Kledzik 说 +load 这个方法已经 deprecated 了,不建议使用,但是去 NSObject Class Reference 看,并没有对此做了什么标记。接着调用 __attribute__((constructor)) 修饰的函数,然后就是对 C++ 中全局对象进行初始化(如下图,Person 的构造函数先于程序的 main() 函数的调用)。

static_cpp_object.png

结束

至此可知,在 main() 函数之前,我们可以通过减少库的数目、减少类的数目、不在+load里面做过多的事情等手段,加快应用的启动速度。在 main() 函数之后,就是要求主线程在 -application:didFinishLaunchingWithOptions: 中尽快返回。

上述的每一个步骤都可以展开很多内容来讲,这里推荐一些比较厉害的博客,sunnyxx 的《iOS 程序 main 函数之前发生了什么》 还有 mikeash.com 上的《Friday Q&A 2012-11-09: dyld: Dynamic Linking On OS X》

我才不会说我把这个视频的字幕翻译了一遍才勉强看得懂咧……🙈

上一篇下一篇

猜你喜欢

热点阅读