iOS App的启动过程
一、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 程序启动原理
iOS app 程序启动原理 Info.plist: 常见设置 建立一个工程后,会在Supporting files文件夹下看到一个"工程名-Info.plist"的文件, ...
- Info.plist和pch文件的作用,UIApplication,iOS程序的启动过程,AppDelegate 方法解释,UIWindow,生命周期方法
Info.plist常见的设置 建立一个工程后,会在Supporting files文件夹下看到一个“工程名-Info.plist”的文件,该文件对工程做一些运行期的配置,非常重要,不能删除 注:在旧 ...
- 怎样做一个iOS App的启动分层引导动画?
一. 为什么要写这篇文章? 这是一个很古老的话题,从两年前新浪微博开始使用多层动画制作iOS App的启动引导页让人眼前一亮(当然,微博是不是历史第一个这个问题值得商榷)之后,各种类型的引导页层出不穷 ...
- App的启动过程
App的启动过程 所有的app都是通过launcher去启动的 launcher自己也是一个app,一个系统级别的app,放在asystem/app/下,使用系统签名. 对代码进行分析
- iOS程序的启动过程介绍
大家在学习iPhone开发时候,都会写HelloWorld程序.大家一般都是通过向导,生成项目,然后通过模拟器启动应用程序.但是大家知道其背后的启动过程吗?也就是当点击程序图标启动程序开始到退出程序整 ...
- iOS程序的启动过程-UIWindow
UIApplicationMain main函数中执行了一个UIApplicationMain这个函数 int UIApplicationMain(int argc, char *argv[], NS ...
- iOS APP程序启动原理
UIApplication 程序启动原理 一个应用程序运行就必须要有一个进程,一个进程至少要有一个线程,我们把这个线程叫做主线程,主线程开启之后会开启一个主运行循环,如果不开启一个运行循环,程序开启了 ...
- iOS开发之iOS程序的启动过程
程序启动的完整过程: (1)main函数 (2)UIApplicationMain * 创建UIApplication对象 * 创建UIApplication的delegate对象 (3)delega ...
- iOS程序的启动过程
UIApplicationMain main函数中执行了一个UIApplicationMain这个函数 int UIApplicationMain(int argc, char *argv[], NS ...
随机推荐
- python下载神通板砖有声版
背景 前端时间在喜马拉雅上偶然听到牛大宝说的有声小说神通板砖,说的很是幽默,听的正起劲的时候开始收费了,于是我就在网上找了下看看有没有免费版,一搜果然有,但该网站上广告太多了,于是我就写了个小脚本可以 ...
- 如何把.a转化为framework
在Xcode中,framework比分散的.a和.h文件用起来方便的多.然而,只要你一找如何制作framework,多半你就会放弃,“怎么这么麻烦?!” 尤其是当已经有现成的.a和.h时,你就会更不能 ...
- 【OO第三次课下讨论】农场主的饲料分配问题
需求分析与项目设计 本思考题的设计需求是力图找到一个简单且可行的饲料分配方案,由于不涉及到饲料价格或者是营养均衡之类的优化问题,因此在假设总的饲料量必能满足所有动物的热量需求的前提下,我们只需要采 ...
- 一文搞懂 Elasticsearch 之 Mapping
这篇文章主要介绍 Mapping.Dynamic Mapping 以及 ElasticSearch 是如何自动判断字段的类型,同时介绍 Mapping 的相关参数设置. 首先来看下什么是 Mappin ...
- 等价类计数:Burnside引理 & Polya定理
提示: 本文并非严谨的数学分析,有很多地方是自己瞎口胡的,仅供参考.有错误请不吝指出 :p 1. 群 1.1 群的概念 群 \((S,\circ)\) 是一个元素集合 \(S\) 和一种二元运算 $ ...
- ILspy不能查看VS代码问题
1.问题情况 刚接触IL,发现从网上下载的IL无法反编译 dll和exe文件都不行 出现如下提示: System.OutOfMemoryException: 数组维度超过了支持的范围. 2.解决方法 ...
- rabbitmq系列(四)死信队列
一.什么是死信队列 当消息在一个队列中变成一个死信之后,它将被重新publish到另一个交换机上,这个交换机我们就叫做死信交换机,私信交换机将死信投递到一个队列上就是死信队列.具体原理如下图: 消息变 ...
- 14. java基于excel模板导出excel=>使用jxls最新版(注意点)
注意点:如下: jxls官网:http://jxls.sourceforge.net/getting_started.html
- Leetcode_1048. 最长字符串链
字符串的最长严格递增子序列,前后只能相差一个字符. 直接O(N^2)暴力建图,然后记忆化跑个最长路. 直接按字符串长度排序,然后求LIS. code1 class Solution { public: ...
- 使用new Image()可以针对单单请求,不要返回数据的情况
使用new Image()可以针对单单请求,不要返回数据的情况,比如我这里写了一个Demo,请求百度的Logo一个示例: <html> <head> </head> ...