iOS底层原理_12应用程序加载

第十二节课 应用程序加载

应用程序的加载原理

首先,我们每次Xcode跑程序的时候不知道大家有没有好奇它这个启动流程到底是什么样子的?

编译过程:


12-加载流程.png

!

  • 源文件:载入.h、.m、.cpp等文件
  • 预处理:替换宏,删除注释,展开头文件,产生.i文件
  • 编译:将.i文件转换为汇编语言,产生.s文件
  • 汇编:将汇编文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件

那么什么是可执行文件呢?我们来到项目中,编译一下,在Products目录下,会出现一个黑不溜秋的文件,这个就是可执行文件。

注:Xcode没有显示Products文件夹的情况下:
1.找到项目文件.xcodeproj
2.右击「显示包内容」
3.打开 project.pbxproj 文件
4.搜索mainGroup
5.将mainGroup后面的value串,作为productRefGroup后面的value串
库:可执行的二进制文件,能够被操作系统加载到内存

静态库与动态库

静态库:在链接阶段,会将可汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的

  • 优点:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行
  • 缺点:由于静态库会有两份,所以会导致目标程序的体积增大,耗费性能

动态库:程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入

  • 优点
  1.  减少打包之后app的大小:因为不需要拷贝至目标程序中,所以不会影响目标程序的体积,与静态库相比,减少了app的体积大小。
    
  2.  共享内存,节约资源:同一份库可以被多个程序使用
    
  3.  通过更新动态库,达到更新程序的目的:由于运行时才载入的特性,可以随时对库进行替换,而不需要重新编译代码,这也是之前总听到的热更新,但是由于苹果的政策后续也进制使用了。
    
  • 缺点:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行

静态库和动态库的图示如图所示


12-静态库动态库.png

加载流程我们知道了,动静态库我们也知道了,但是这些库是怎么加载到程序中的呢?这就是我们今天要研究的dyld链接器了。

dyld::_main函数源码分析

12-dyld.png

dyld(the dynamic link editor)是苹果的动态链接器,主要是链接一些动静态库的一个工具。那我们怎么去找到并分析了解dyld呢?这就需要我们通过源码的形式去了解了。

我们先写一个main函数,并打一个断点.断住后我们去看下进程

int main(int argc, char * argv[]) {
}

我们发现在main之前还有一个start的进程


12-start.png

而这个start,我们只能看到


12-dyld_start.png

如果我们想更精准的定位到的话,按照以前的经验就是下一个start的符号断点,但是,尝试后发现并没有断到start,那就说明,这个dylib.start底层并不是start。那么我们只能通过main函数之前现执行的load方法来进行断点了。

12-load.png

断住后,控制台输入bt查看


12-bt.png

在最下面我们可以看到,_dyld_start,这个其实就是我们要找的start方法。
接下来我们需要下载一下dyld的源码进行下一步的分析(官方网址:https://opensource.apple.com/tarballs/dyld/)

dyld流程

打开源码后,我们搜索_dyld_start,发现有很多结果,但是仔细看一下,其实是区分了不同架构的。我们选择一个进行分析

12-dyld源码.png

这么些汇编语言,我们又不懂,怎么办呢?其实我们只要看它的主要调用方法就行了。也就是下图中的call后面的方法dyldbootstrap::start

12-dyldbootstrap.png

接下来因为C++的一些语法问题,我们如果直接搜索dyldbootstrap::start是搜不到的,我们只能先搜索dyldbootstrap,再去找start函数。

12-dyldbootstrapstart.png

![12-return.png](https://upload-images.jianshu.io/upload_images/24944531-435327e63c9ec942.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

根据我们经验,直接看return,然后进入_main函数查看

进入dyld::_main后,代码很长,我们还是通过看return的返回值,进行反推,我们来到最下面看到的是return result;,所以我们全局搜索一下result,先从这里入手

12-result.png

我们会看到这样两个地方,可以看出,关键的点就在sMainExecutable这个地方。
继续搜索sMainExecutable =,搜索后发现了一条实例化的语法如下图

实例化对象

12-sMainExecutable .png
12-instantiateFromLoadedImage.png

进入instantiateMainExecutable源码,其作用是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序。其中sniffLoadCommands函数时获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验。

12-instantiateMainExecutable.png

现在我们看完了实例化主程序instantiateMainExecutable,再回到_main函数往下看

插入动态库

遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载


12-loadInsertedDylib.png

link 主程序和动态库

12-link主程序和动态库.png

弱符号绑定

12-weakBind.png

执行初始化

12-initializeMainExecutable.png

进入后发现里面有一个循环遍历执行的方法runInitializers

全局搜索runInitializers(cons,找到如下源码,其核心代码是processInitializers函数的调用

12-runInitializers.png

进入processInitializers函数的源码实现,其中对镜像列表调用recursiveInitialization函数进行递归实例化

12-processInitializers.png

全局搜索recursiveInitialization(cons函数,其源码实现如下

12-recursiveInitialization.png

可以看到上半部分的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函数中

12-notifySingle.png

全局搜索sNotifyObjCInit,只找到一个赋值操作

12-registerObjCNotifiers.png

被赋予的值是来自于registerObjCNotifiers的第二个参数,我们再继续反推上去

12-_dyld_objc_notify_register.png

在objc源码中搜索_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images,而load_images会调用所有的+load方法。所以综上所述,notifySingle是一个回调函数

12-_objc_init.png

通过断点定位到_objc_init上一级发起的点libdispatch源码当中

12-libdispatch.png

进入libdispatch源码中,搜索_objc_init

12-_os_object_init.png

12-bt线程.png

根据堆栈信息继续往前推,来到了libdispatch_init

12-libdispatch_init.png

再往前,来到了LibsystemlibSystem_initializer

12-libSystem_initializer.png

继续看堆栈,发现又来到了dylddoModInitFunctions

12-doModInitFunctions.png

我们可以看到有这么一段


12-判断libSystem是否加载完成.png

判断libSystem是否加载完成,否则报错,这也证明我们之前的推断是正确的,libSystem_initializer加载之后才能继续加载别的库。

继续往前推,我们来到了doInitialization,同时在bt的堆栈中也可以看到相应的线程

12-doInitialization.png

再往前找到doInitialization被调用的地方

12-recursiveInitialization.png

我们发现,又回来了~这不就是之前开始地方,哈哈哈。完美的形成了一个闭环,验证了我们之前的所有推断。

通知dyld可以进main函数了

回到我们的_main函数当我们跑完了所有的初始化,我们也就可以通知其他监听者,即将进入main函数。

12-notifyMonitoringDyldMain.png

整体流程图整理如下:


12-dyld流程总结.png

其实转了这么一大圈,我们还是懵逼的状态,我们根本不知道它到底是怎么做到的,只知道这是启动前的这一个必要的工序,但是这个探索的过程,让我们了解到了dyld的大致流程,同时我们也学习到了一种反推模式的推敲方式,这也是我们之后经常会用到的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容