一、mach-O

  • Executable 可执行文件
  • Dylib 动态库
  • Bundle 无法被连接的动态库,只能通过 dlopen() 加载
  • Image 指的是 Executable,Dylib 或者 Bundle 的一种。
  • Framework 动态库和对应的头文件和资源文件的集合

Apple 出品的操作系统的可执行文件格式几乎都是 mach-O。

mach-O 可以大致分为三部分:

  • Header 头部,包含可以执行的 CPU 架构,比如 x86,arm64
  • Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
  • Data 数据,包含 load commands 中需要的各个段(segment)的数据,每个 Segment 的大小都是 Page 的整数倍。

用 MachOView 打开 Demo 工程的可以执行文件,来验证下 mach-O 的文件布局:

图中分析的 mach-O 文件来源于 PullToRefreshKit。这是一个纯 Swift 的编写的工程。

那么 Data 部分又包含那些 segment 呢?绝大多数 mach-O 包括以下三个阶段(支持用户自定义Segment,但是很少使用)

  • __TEXT 代码段,只读。包含函数和只读的字符串,上图中类似 __TEXT,__text 的都是代码段
  • __Data 数据段,读写。包括可读写的全局变量等,__DATA,__data 都是数据段
  • __LINKEDIT 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。

关于 mach-O 更多细节,可以看看文档:《Mac OS X ABI Mach-O File Format Reference》

二、dyld

dyld 的全称是 dynamic loader,它的作用是加载一个进程所需要的 image,dyld 是开源的

三、Virtual Memory

虚拟内存是在物理内存上建立的一个逻辑地址空间,它向上(应用)提供了一个连续的逻辑地址空间,向下隐藏了物理内存的细节。
虚拟内存使得逻辑地址可以没有实际的物理地址,也可以让多个逻辑地址对应到一个物理地址。
虚拟内存被划分为一个个大小相同的 Page(64 位系统上是 16KB),提高管理和读写的效率。 Page 又分为只读和读写的 Page。

虚拟内存是建立在物理内存和进程之间的中间层。在 iOS 上,当内存不足的时候,会尝试释放那些只读的 Page,因为只读的 Page 在下次被访问的时候,可以再从磁盘读取。如果没有可用内存,会通知在后台的App(也就是在这个时候收到了memory warning),如果在这之后仍然没有可用内存,则会杀死在后台的App。

四、Page fault

在应用执行的时候,它被分配的逻辑地址空间都是可以访问的,当应用访问一个逻辑 Page,而在对应的物理内存中并不存在的时候,这时候就发生了一次 Page fault。当 Page fault 发生的时候,会中断当前的程序,在物理内存中寻找一个可用的 Page,然后从磁盘中读取数据到物理内存,接着继续执行当前程序。

五、Dirty Page & Clean Page

  • 如果一个 Page 可以从磁盘上重新生成,那么这 Page 称为 Clear Page
  • 如果一个 Page 包含了进程相关信息,那么这个 Page 称为 Dirty Page

像代码段这种只读的 Page 就是 Clean Page。而数据段(__DATA)这种读写的 Page,当写数据发生的时候,会触发 CO(Copy on write),也就是写时复制,Page 会被标记成 Dirty,同时会被复制。

想要了解更多细节,可以阅读文档:Memory Usage Performance Guidelines

六、启动过程

使用 dyld2 启动应用的过程如图:

大致的过程如下:

  • 加载 dyld 到 App 进程
  • 加载动态库(包括所依赖的所有动态库)
  • Rebase
  • Bind
  • 初始化 Objective-C Runtime
  • 其它的初始化代码

6.1 加载动态库

dyld 会首先读取 mach-O 文件的 Header 和 load commands。

接着就知道了这个可执行文件依赖的动态库。例如加载动态库 A 到内存,接着检查 A 所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个 App 所依赖的动态库在 100~400 个左右,其中大多数都是系统的动态库,它们会被缓存到 dyld shared cache,这样读取的效率会很高。

查看 mach-O 文件所依赖的动态库,可以通过 MachOView 的图形化界面(展开 Load Command 就能看到),也可以通过命令行 otool。

$ otool -L demo
demo:
@rpath/PullToRefreshKit.framework/PullToRefreshKit (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1444.12.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
@rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 900.0.65)
@rpath/libswiftCoreAudio.dylib (compatibility version 1.0.0, current version 900.0.65)
//...

6.2 Rebase && Bind

先讲讲为什么要 Rebase。

有两种主要的技术来保证应用的安全:ASLR 和 Code Sign。

ASLR 的全称是 Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而 ASLR 技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址 + 偏移量找到函数的地址。

Code Sign 相信大多数开发者都知晓,这里要提一点的是,在进行 Code sign 的时候,加密哈希不是针对于整个文件,而是针对于每一个 Page 的。这就保证了在 dyld 进行加载的时候,可以对每一个 page 进行独立的验证。

mach-O 中有很多符号,有指向当前 mach-O 的,也有指向其他 dylib 的,比如 printf。那么,在运行时,代码如何准确的找到 printf 的地址呢?

mach-O 中采用了 PIC 技术,全称是 Position Independ code。当你的程序要调用 printf 的时候,会先在 __DATA 段中建立一个指针指向 printf,在通过这个指针实现间接调用。dyld 这时候需要做一些 fix-up 工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分

  • Rebase 修正内部(指向当前 mach-O 文件)的指针指向
  • Bind 修正外部指针指向

之所以需要 Rebase,是因为刚刚提到的 ASLR 使得地址随机化,导致起始地址不固定,另外由于 Code Sign,导致不能直接修改 Image。Rebase 的时候只需要增加对应的偏移量即可。待 Rebase 的数据都存放在 __LINKEDIT 中。

可以通过 MachOView 查看:Dynamic Loader Info -> Rebase Info

$ xcrun dyldinfo -bind demo
bind information:
segment section address type addend dylib symbol
__DATA __got 0x10003C038 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC4LeftC9textLabelSo7UILabelCvWvd
__DATA __got 0x10003C040 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC5RightC9textLabelSo7UILabelCvWvd
__DATA __got 0x10003C048 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC6FooterC9textLabelSo7UILabelCvWvd
__DATA __got 0x10003C050 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC6HeaderC7spinnerSo23UIActivityIndicatorViewCvWvd
//...

Rebase 解决了内部的符号引用问题,而外部的符号引用则是由 Bind 解决。在解决 Bind 的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于 Rebase 来说是略慢的。

同样,也可以通过 xcrun dyldinfo 来查看 Bind 的信息,比如我们查看 bind 信息中,包含 UITableView 的部分:

$ xcrun dyldinfo -bind demo | grep UITableView
__DATA __objc_classrefs 0x100041940 pointer 0 UIKit _OBJC_CLASS_$_UITableView
__DATA __objc_classrefs 0x1000418B0 pointer 0 UIKit _OBJC_CLASS_$_UITableViewCell
__DATA __objc_data 0x100041AC0 pointer 0 UIKit _OBJC_CLASS_$_UITableViewController
__DATA __objc_data 0x100041BE8 pointer 0 UIKit _OBJC_CLASS_$_UITableViewController
__DATA __objc_data 0x100042348 pointer 0 UIKit _OBJC_CLASS_$_UITableViewController
__DATA __objc_data 0x100042718 pointer 0 UIKit _OBJC_CLASS_$_UITableViewController
__DATA __data 0x100042998 pointer 0 UIKit _OBJC_METACLASS_$_UITableViewController
__DATA __data 0x100042A28 pointer 0 UIKit _OBJC_METACLASS_$_UITableViewController
__DATA __data 0x100042F10 pointer 0 UIKit _OBJC_METACLASS_$_UITableViewController
__DATA __data 0x1000431A8 pointer 0 UIKit _OBJC_METACLASS_$_UITableViewController

6.3 Objective-C

Objective-C 是动态语言,所以在执行 main 函数之前,需要把类的信息注册到一个全局的 Table 中。同时,Objective-C 支持 Category,在初始化的时候,也会把 Category 中的方法注册到对应的类中,同时会唯一 Selector,这也是为什么当你的 Cagegory 实现了类中同名的方法后,类中的方法会被覆盖。

另外,由于 iOS 开发是基于 Cocoa Touch 的,所以绝大多数的类起始都是系统类,所以大多数的 Runtime 初始化起始在 Rebase 和 Bind 中已经完成。

6.4 Initializers

接下来就是必要的初始化部分了,主要包括几部分:

  • +load方法。
  • C/C++ 静态初始化对象和标记为 attribute(constructor) 的方法

+load 方法已经被弃用了,如果你用 Swift 开发,你会发现根本无法去写这样一个方法,官方的建议是使用 initialize。区别就是,load 是在类装载的时候执行,initialize 是在类第一次收到 message 前调用。

6.5 dyld3

上面讲解是 dyld2 的加载方式。而最新的是 dyld3 加载方式略有不同:

dyld2 是纯粹的 in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时 dyld2 才能开始执行任务。

dyld3 则是部分 out-of-process,部分 in-process。图中,虚线之上的部分是 out-of-process的,在 App 下载安装和版本更新的时候会去执行,out-of-process 会做如下事情:

  • 分析 Mach-o Headers
  • 分析依赖的动态库
  • 查找需要 Rebase & Bind 之类的符号
  • 把上述结果写入缓存

这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。

文章

为自己丶拼个未来 - 深入理解iOS App的启动过程

iOS App的启动过程的更多相关文章

  1. iOS app 程序启动原理

    iOS app 程序启动原理 Info.plist: 常见设置     建立一个工程后,会在Supporting files文件夹下看到一个"工程名-Info.plist"的文件, ...

  2. Info.plist和pch文件的作用,UIApplication,iOS程序的启动过程,AppDelegate 方法解释,UIWindow,生命周期方法

    Info.plist常见的设置 建立一个工程后,会在Supporting files文件夹下看到一个“工程名-Info.plist”的文件,该文件对工程做一些运行期的配置,非常重要,不能删除 注:在旧 ...

  3. 怎样做一个iOS App的启动分层引导动画?

    一. 为什么要写这篇文章? 这是一个很古老的话题,从两年前新浪微博开始使用多层动画制作iOS App的启动引导页让人眼前一亮(当然,微博是不是历史第一个这个问题值得商榷)之后,各种类型的引导页层出不穷 ...

  4. App的启动过程

    App的启动过程 所有的app都是通过launcher去启动的 launcher自己也是一个app,一个系统级别的app,放在asystem/app/下,使用系统签名. 对代码进行分析

  5. iOS程序的启动过程介绍

    大家在学习iPhone开发时候,都会写HelloWorld程序.大家一般都是通过向导,生成项目,然后通过模拟器启动应用程序.但是大家知道其背后的启动过程吗?也就是当点击程序图标启动程序开始到退出程序整 ...

  6. iOS程序的启动过程-UIWindow

    UIApplicationMain main函数中执行了一个UIApplicationMain这个函数 int UIApplicationMain(int argc, char *argv[], NS ...

  7. iOS APP程序启动原理

    UIApplication 程序启动原理 一个应用程序运行就必须要有一个进程,一个进程至少要有一个线程,我们把这个线程叫做主线程,主线程开启之后会开启一个主运行循环,如果不开启一个运行循环,程序开启了 ...

  8. iOS开发之iOS程序的启动过程

    程序启动的完整过程: (1)main函数 (2)UIApplicationMain * 创建UIApplication对象 * 创建UIApplication的delegate对象 (3)delega ...

  9. iOS程序的启动过程

    UIApplicationMain main函数中执行了一个UIApplicationMain这个函数 int UIApplicationMain(int argc, char *argv[], NS ...

随机推荐

  1. python下载神通板砖有声版

    背景 前端时间在喜马拉雅上偶然听到牛大宝说的有声小说神通板砖,说的很是幽默,听的正起劲的时候开始收费了,于是我就在网上找了下看看有没有免费版,一搜果然有,但该网站上广告太多了,于是我就写了个小脚本可以 ...

  2. 如何把.a转化为framework

    在Xcode中,framework比分散的.a和.h文件用起来方便的多.然而,只要你一找如何制作framework,多半你就会放弃,“怎么这么麻烦?!” 尤其是当已经有现成的.a和.h时,你就会更不能 ...

  3. 【OO第三次课下讨论】农场主的饲料分配问题

    需求分析与项目设计   本思考题的设计需求是力图找到一个简单且可行的饲料分配方案,由于不涉及到饲料价格或者是营养均衡之类的优化问题,因此在假设总的饲料量必能满足所有动物的热量需求的前提下,我们只需要采 ...

  4. 一文搞懂 Elasticsearch 之 Mapping

    这篇文章主要介绍 Mapping.Dynamic Mapping 以及 ElasticSearch 是如何自动判断字段的类型,同时介绍 Mapping 的相关参数设置. 首先来看下什么是 Mappin ...

  5. 等价类计数:Burnside引理 & Polya定理

    提示: 本文并非严谨的数学分析,有很多地方是自己瞎口胡的,仅供参考.有错误请不吝指出 :p 1. 群 1.1 群的概念 群 \((S,\circ)\) 是一个元素集合 \(S\) 和一种二元运算 $ ...

  6. ILspy不能查看VS代码问题

    1.问题情况 刚接触IL,发现从网上下载的IL无法反编译 dll和exe文件都不行 出现如下提示: System.OutOfMemoryException: 数组维度超过了支持的范围. 2.解决方法 ...

  7. rabbitmq系列(四)死信队列

    一.什么是死信队列 当消息在一个队列中变成一个死信之后,它将被重新publish到另一个交换机上,这个交换机我们就叫做死信交换机,私信交换机将死信投递到一个队列上就是死信队列.具体原理如下图: 消息变 ...

  8. 14. java基于excel模板导出excel=>使用jxls最新版(注意点)

    注意点:如下: jxls官网:http://jxls.sourceforge.net/getting_started.html

  9. Leetcode_1048. 最长字符串链

    字符串的最长严格递增子序列,前后只能相差一个字符. 直接O(N^2)暴力建图,然后记忆化跑个最长路. 直接按字符串长度排序,然后求LIS. code1 class Solution { public: ...

  10. 使用new Image()可以针对单单请求,不要返回数据的情况

    使用new Image()可以针对单单请求,不要返回数据的情况,比如我这里写了一个Demo,请求百度的Logo一个示例: <html> <head> </head> ...