第十二节课 应用程序加载
应用程序的加载原理
首先,我们每次Xcode跑程序的时候不知道大家有没有好奇它这个启动流程到底是什么样子的?
编译过程:
!
-
源文件
:载入.h、.m、.cpp等文件 -
预处理
:替换宏,删除注释,展开头文件,产生.i文件 -
编译
:将.i文件转换为汇编语言,产生.s文件 -
汇编
:将汇编文件转换为机器码文件,产生.o文件 -
链接
:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
那么什么是可执行文件呢?我们来到项目中,编译一下,在Products目录下,会出现一个黑不溜秋的文件,这个就是可执行文件。
注:Xcode没有显示Products文件夹的情况下:
1.找到项目文件.xcodeproj
2.右击「显示包内容」
3.打开 project.pbxproj 文件
4.搜索mainGroup
5.将mainGroup后面的value串,作为productRefGroup后面的value串
库:可执行的二进制文件,能够被操作系统加载到内存
静态库与动态库
静态库
:在链接阶段,会将可汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的
-
优点
:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行 -
缺点
:由于静态库会有两份,所以会导致目标程序的体积增大,耗费性能
动态库
:程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入
-
优点
:
减少打包之后app的大小:因为不需要拷贝至目标程序中,所以不会影响目标程序的体积,与静态库相比,减少了app的体积大小。
共享内存,节约资源:同一份库可以被多个程序使用
通过更新动态库,达到更新程序的目的:由于运行时才载入的特性,可以随时对库进行替换,而不需要重新编译代码,这也是之前总听到的热更新,但是由于苹果的政策后续也进制使用了。
-
缺点
:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行
静态库和动态库的图示如图所示
加载流程我们知道了,动静态库我们也知道了,但是这些库是怎么加载到程序中的呢?这就是我们今天要研究的dyld链接器了。
dyld::_main函数源码分析
dyld(the dynamic link editor)
是苹果的动态链接器,主要是链接一些动静态库的一个工具。那我们怎么去找到并分析了解dyld呢?这就需要我们通过源码的形式去了解了。
我们先写一个main
函数,并打一个断点.断住后我们去看下进程
int main(int argc, char * argv[]) {
}
我们发现在main之前还有一个start的进程
而这个start,我们只能看到
如果我们想更精准的定位到的话,按照以前的经验就是下一个start
的符号断点,但是,尝试后发现并没有断到start
,那就说明,这个dylib.start
底层并不是start
。那么我们只能通过main
函数之前现执行的load
方法来进行断点了。
断住后,控制台输入bt查看
在最下面我们可以看到,_dyld_start
,这个其实就是我们要找的start
方法。
接下来我们需要下载一下dyld的源码
进行下一步的分析(官方网址:https://opensource.apple.com/tarballs/dyld/
)
dyld流程
打开源码后,我们搜索_dyld_start
,发现有很多结果,但是仔细看一下,其实是区分了不同架构的。我们选择一个进行分析
这么些汇编语言,我们又不懂,怎么办呢?其实我们只要看它的主要调用方法就行了。也就是下图中的call后面的方法dyldbootstrap::start
。
接下来因为C++的一些语法问题,我们如果直接搜索dyldbootstrap::start
是搜不到的,我们只能先搜索dyldbootstrap
,再去找start
函数。
根据我们经验,直接看return
,然后进入_main
函数查看
进入dyld::_main
后,代码很长,我们还是通过看return
的返回值,进行反推,我们来到最下面看到的是return result;
,所以我们全局搜索一下result
,先从这里入手
我们会看到这样两个地方,可以看出,关键的点就在sMainExecutable
这个地方。
继续搜索sMainExecutable =
,搜索后发现了一条实例化的语法如下图
实例化对象
进入instantiateMainExecutable
源码,其作用是为主可执行文件创建映像
,返回一个ImageLoader
类型的image
对象,即主程序。其中sniffLoadCommands
函数时获取Mach-O
类型文件的Load Command
的相关信息,并对其进行各种校验。
现在我们看完了实例化主程序instantiateMainExecutable
,再回到_main
函数往下看
插入动态库
遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载
link 主程序和动态库
弱符号绑定
执行初始化
进入后发现里面有一个循环遍历
执行的方法runInitializers
全局搜索runInitializers(cons
,找到如下源码,其核心代码是processInitializers
函数的调用
进入processInitializers
函数的源码实现,其中对镜像列表调用recursiveInitialization
函数进行递归实例化
全局搜索recursiveInitialization(cons
函数,其源码实现如下
可以看到上半部分的for循环是先做一些初始化低级库的过程,所以不是很重要。而下面的注释也提提醒了我们let objc know we are about to initialize this image
所以下面的这部分是这段源码的重点。
全局搜索notifySingle(
函数,进入notifySingle
源码,依旧是根据找重点代码
第一段if函数是拿到各项数据
,第二段if函数是处理共享缓存与UUID
的,第三段if函数当中我们看到了在上一层出现的state == dyld_image_state_dependents_initialized
,而最后一段if函数是unloaded images的处理
。所以重点函数必定在第三段if函数中
。
全局搜索sNotifyObjCInit
,只找到一个赋值操作
被赋予的值是来自于registerObjCNotifiers
的第二个参数,我们再继续反推上去
在objc源码中搜索_dyld_objc_notify_register
,发现在_objc_init
源码中调用了该方法,并传入了参数,所以sNotifyObjCInit
的赋值的就是objc中的load_images
,而load_images
会调用所有的+load
方法。所以综上所述,notifySingle
是一个回调函数
通过断点定位到_objc_init
上一级发起的点libdispatch源码
当中
进入libdispatch源码
中,搜索_objc_init
根据堆栈信息继续往前推,来到了libdispatch_init
再往前,来到了Libsystem
的libSystem_initializer
继续看堆栈,发现又来到了dyld
的doModInitFunctions
我们可以看到有这么一段
判断libSystem
是否加载完成,否则报错,这也证明我们之前的推断是正确的,libSystem_initializer
加载之后才能继续加载别的库。
继续往前推,我们来到了doInitialization
,同时在bt的堆栈中也可以看到相应的线程
再往前找到doInitialization
被调用的地方
我们发现,又回来了~这不就是之前开始地方,哈哈哈。完美的形成了一个闭环,验证了我们之前的所有推断。
通知dyld可以进main函数了
回到我们的_main
函数当我们跑完了所有的初始化,我们也就可以通知其他监听者,即将进入main
函数。
整体流程图整理如下:
其实转了这么一大圈,我们还是懵逼的状态,我们根本不知道它到底是怎么做到的,只知道这是启动前的这一个必要的工序,但是这个探索的过程,让我们了解到了dyld的大致流程,同时我们也学习到了一种反推模式的推敲方式,这也是我们之后经常会用到的。