main()调用之前的加载过程
App
开始启动后,系统首先加载可执行文件(自身App
的所有.o
文件的集合),然后加载动态链接库dyld
。
dyld
是一个专门用来加载动态链接库的库。
dyld源码链接
执行从dyld
开始,dyld
从可执行文件的依赖开始, 递归加载所有的依赖动态链接库。
动态链接库包括:
-
iOS
中用到的所有系统framework
, - 加载
OC runtime
方法的libobjc
, - 系统级别的
libSystem
,例如libdispatch(GCD)
和libsystem_blocks (Block)
。
其实无论对于系统的动态链接库还是对于App
本身的可执行文件而言,他们都算是image
(镜像),而每个App
都是以image
(镜像)为单位进行加载的。
什么是image(镜像)
1.executable
可执行文件 比如.o
文件。
2.dylib
动态链接库framework
就是动态链接库和相应资源包含在一起的一个文件夹结构。
3.bundle
资源文件 只能用dlopen
加载,不推荐使用这种方式加载。
除了我们App
本身的可行性文件,系统中所有的framework
比如UIKit
、Foundation
等都是以动态链接库的方式集成进App
中的。
系统使用动态链接有几点好处:
代码共用:很多程序都动态链接了这些 lib
,但它们在内存和磁盘中中只有一份。 易于维护:由于被依赖的 lib
是程序执行时才链接的,所以这些 lib
很容易做更新,比如libSystem.dylib
是 libSystem.B.dylib
的替身,哪天想升级直接换成libSystem.C.dylib
然后再替换替身就行了。 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多。
如上图所示,不同进程之间共用系统dylib
的_TEXT
区,但是各自维护对应的_DATA
区。
所有动态链接库和我们App
中的静态库.a
和所有类文件编译后的.o
文件最终都是由dyld(the dynamic link editor)
,Apple
的动态链接器来加载到内存中。每个image
都是由一个叫做ImageLoader
的类来负责加载(一一对应),那么ImageLoader
又是什么呢?
什么是ImageLoader
image
表示一个二进制文件(可执行文件或 so
文件),里面是被编译过的符号、代码等,所以 ImageLoader
作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader
实例来负责加载。
两步走: 在程序运行时它先将动态链接的 image
递归加载 (也就是上面测试栈中一串的递归调用的时刻)。 再从可执行文件 image
递归加载所有符号。
当然所有这些都发生在我们真正的main
函数执行前。
动态链接库加载的具体流程
动态链接库的加载步骤具体分为5步:
1.load dylibs image
读取库镜像文件
2.Rebase image
3.Bind image
4.Objc setup
5.initializers
1.load dylibs image
在每个动态库的加载过程中, dyld
需要:
1.分析所依赖的动态库
2.找到动态库的mach-o
文件
3.打开文件
4.验证文件
5.在系统核心注册文件签名
6.对动态库的每一个segment
调用mmap()
通常的,一个App
需要加载100到400个dylibs
, 但是其中的系统库被优化,可以很快的加载。
针对这一步骤的优化有:
1.减少非系统库的依赖
2.合并非系统库
3.使用静态资源,比如把代码加入主程序
2.rebase/bind
由于ASLR(address space layout randomization)
的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。
rebase
修复的是指向当前镜像内部的资源指针;
而bind指向的是镜像外部的资源指针。
rebase步骤先进行,需要把镜像读入内存,并以page
为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO
。
bind
在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase
阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU
计算。
//通过命令行可以查看相关的资源指针:
xcrun dyldinfo -rebase -bind -lazy_bind myApp.App/myApp
优化该阶段的关键在于减少__DATA segment
中的指针数量。
可以优化的点有:
1.减少Objc
类数量, 减少selector
数量
2.减少C++
虚函数数量
3.转而使用swift stuct
(其实本质上就是为了减少符号的数量)
3.Objc setup
这一步主要工作是:
1.注册Objc
类 (class registration
)
2.把category
的定义插入方法列表 (category registration
)
3.保证每一个selector
唯一 (selctor uniquing
)
4.由于之前2步骤的优化,这一步实际上没有什么可做的。
4.initializers
以上三步属于静态调整(fix-up
),都是在修改__DATA segment
中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。 在这里的工作有:
1.Objc
的+load()
函数
2.C++
的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
3.非基本类型的C++
静态全局变量的创建(通常是类或结构体)(non-trivial initializer
) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度
Objc
的load
函数和C++
的静态构造函数采用由底向上的方式执行,来保证每个执行的方法,都可以找到所依赖的动态库。
1).dyld
开始将程序二进制文件初始化
2).交由 ImageLoader
读取 image
,其中包含了我们的类、方法等各种符号
3).由于 runtime
向 dyld
绑定了回调,当 image
加载到内存后,dyld
会通知 runtime
进行处理
4).runtime
接手后调用 mapimages
做解析和处理,接下来 loadimages
中调用 callloadmethods
方法,遍历所有加载进来的 Class
,按继承层级依次调用 Class
的 +load
方法和其 Category
的 +load
方法
至此
至此,可执行文件中和动态库所有的符号(Class
,Protocol
,Selector
,IMP
,…)都已经按格式成功加载到内存中,被 runtime
所管理,再这之后,runtime
的那些方法(动态添加 Class
、swizzle
等等才能生效)。
整个事件由 dyld
主导,完成运行环境的初始化后,配合 ImageLoader
将二进制文件按格式加载到内存, 动态链接依赖库,并由 runtime
负责加载成 objc
定义的结构,所有初始化工作结束后,dyld
调用真正的 main
函数。
结语
以上就是这篇文章的全部内容了,希望本文的内容对大家具有一定的参考学习价值,同时欢迎大家进入小编iOS交流圈:937194184,一起交流学习,谢谢大家的支持。
原文地址