iOS应用程序加载流程探究

226 阅读6分钟

前言

在开发界有个很著名的常识就是代码都是从main函数开始执行,iOS也不例外,程序的入口也是在main.m文件内的main函数,那么从用户点击app图标启动应用到main函数开始执行这个过程发生了什么呢,今天我们来探究一下。

准备工作

苹果开源网站opensource.apple.com/

  1. objc源码
  2. dyld源码
  3. libdispatch源码
  4. Libsystem源码

加载流程探索

首先我们创建一个singleview project单页面工程,这里Xcode自动为我们生成了代码,在main.m中的main函数开始打个断点,运行,程序断在了断点位置,lldb调试命令输入bt查看调用栈,如下:

image.png

通过调用栈我们发现在main函数执行之前执行的dyld内的start函数,我们我们点击Xcode左侧导航的调用栈看看start里边是什么样的

image.png

可以看到汇编里调用了一些c++函数,我们打开dyld源码,搜索一下试试看能不能定位到start(ps:为什么不直接在源码里搜索start,start不够特殊搜索出来的东西有一大坨不好定位)

image.png

通过搜索我们定位到了dyld的入口,这里可以看到一些初始化的操作,然后直接返回dyld的main函数执行,直接点击跳转看看这个main里边做了那些事情,这个main函数的代码非常的长,其中包括很多环境变量处理、调试器追踪处理、运行平台判断、CPU架构判断等等代码,我们可以通过代码折叠跳过一些我们不关心的代码,最终在如下位置看到这样一段代码

image.png

通过代码和注释可以看到,这里通过读取系统环境变量里的动态库然后遍历出来,执行loadInsertedDylib函数加载dylib,我们跳转进去看看

image.png

这里看到dylib是通过路径path来加载的,继续跳转看看load

image.png

image.png

这里我么通过注释可以看到这个函数主要是通过算法控制dylib只加载一次,最终返回ImageLoader,我们回到main函数继续往下看

image.png

这里我们看到开始遍历load过的dylib然后执行link链接,继续往下走,看到这样一句代码

image.png

跳转进去看看执行了那些初始化操作

image.png

这里可以看到通过循环执行了dylib的初始化和主程序的初始化,执行的是同一函数runInitializers,继续跳转看一下这里边做了什么

image.png

继续跳转到processInitializers

image.png

image.png

这里循环执行recursiveInitialization递归初始化函数,这个函数跳转不进去了,全局搜索找一下

image.png

这里找到多个实现,通过recursiveInitialization是由ImageLoader调用确定实现,在这里可以看到初始化镜像之前通知了objc,跳转到notifySingle函数内部再看一下

image.png

我们看到这里调用了sNotifyObjCInit方法通知objc,我们再来看看这里边执行了什么

image.png

跳转进去发现是这么个东西,全局搜索一下,找到这样的代码

image.png

通过函数名称我们可以看到dyld在这里注册了通知objc的回调函数,_dyld_objc_notify_init这个东西就是回调函数,sNotifyObjCInit就是_dyld_objc_notify_init,继续跳转到registerObjCNotifiers

image.png

可以看到函数实现对回调进行了赋值,那么现在的问题是这个注册通知回调是什么时候在哪注册的,收到通知后做了哪些事情。全局搜索_dyld_objc_notify_register这个注册函数找不到调用的位置,那这个函数是注册objc的通知,可以猜测是由objc来调用的,那我们打开objc源码搜索一下,找到这个位置

image.png

可以看到这里的_objc_init是由libsystem调用,在这个函数内执行objc初始化过程,之后调用_dyld_objc_notify_register注册通知,那么我们可以看到,这里传入的参数load_images,对应的就是dyld中的sNotifyObjCInit,也就是说在dyld中实际调用的是这里的load_images。那么跳转进入这个函数看看收到dyld通知后都做些什么

image.png

可以看到这里通过macho去加载类并调用+load方法,这也就是+load方法会在main函数执行之前调用的原因。为了搞清楚完整的调用顺序,我们必须知道_objc_init在什么时候调用的,根据这个函数的注释,我们打开libsystem的源码,全局搜索

image.png

毛也没有搜到,苹果这不是坑人么,注释写了又找不到,别急稳住,objc源码是可编译的,打个断点看下调用栈不就完了吗,运行源码程序,bt命令查看,结果如下

image.png

看到这个调用栈就非常的清晰了,_objc_init是由libdispatch调用的,libdispatch是由libsystem调用的,libsystem是由dyld的doModInitFunctions调用的,那我们回到dyld源码追溯一下

image.png

image.png

到这里就发现一个奇怪的问题notifySingle也就是sNotifyObjCInit是在doInitialization之前调用的,也就是说在回调函数没有被注册的时候就被调用了。回到sNotifyObjCInit调用的位置

image.png

这里是由非空判断的,而且recursiveInitialization函数是递归调用的,在首次递归加载image时,没有依赖所以也不需要通知objc依赖加载完成,所以首次不会调用通知而是执行doInitialization调用objc的通知注册函数,在下次递归时,执行sNotifyObjCInit通知objc去加载类等操作。

那我们回到dyld的main函数,在执行完initializeMainExecutable之后,获取了result并在最后return,我们看下这个result是什么

image.png

这里通过getEntryFromLC_MAIN函数拿到main函数地址,搜索一下看看这里边是怎么找到的

image.png

这里通过macho的LC_MAIN对比找到main函数地址,dyld的main函数最终return程序main函数地址,那么最终应用程序的main函数何时调用我们可以在dyldStartup汇编文件找到答案

image.png

总结

整个app启动到main函数执行的过程可以概括为如下流程:

首先由dyld汇编进入dyld的start函数,start函数做一些环境变量处理然后转到dyld的_main函数,在dyld的_main函数内执行dylib的加载、链接、初始化等操作,初始化过程会调用libobjc注册通知并在镜像加载完成时通知objc,objc收到通知进行类的加载相关操作并调用+load方法,最后在dyld的_main函数内通过macho获取到app的main函数地址并return,在dyld的汇编流程调用执行app的main函数。