iOS底层源码分析启动流程

1,702 阅读8分钟

一、课程引入

创建一个工程,在ViewController中重写load方法,在main中加了一个C++方法 mideaCXXFun,同时在load方法、mideaCXXFun和main方法中添加log日志,下面我们来 执行此demo,看一下log日志的打印结果:

image.png

从打印的日志可以看到执行的流程如下:

image.png

  • 1.为什么是这么个调用顺序呢?按照常规思维理解,main不是入函数吗?为什么不是main最先执行?
  • 2.mideaCXXFun函数没有调用,为什么会自动执行呢?

带着这些疑问,开始进入iOS底层源码启动流程的探索!!!

二、编绎过程和库

编译过程如下所示,主要分为以下几步:

image.png

静态库和动态库

静态库

在链接阶段,会将可汇编生成的目标程序与引用的库一起链接打包到可执行文件上中,静态库相当于被直接拷贝 了一份复制到到目标程序里。

  • 优点:目标程序没有外部依赖,直接可以运行;
  • 缺点:目标程序的体积增大,影响程序的内存、性能、速度。

动态库

程序在编译时不会将动态库拷贝到目标程序中,目标程序只会保存动态库的引用,在程序运行时才加载。

  • 优点:减小包大小、共享内存,节约资源、通过更新动态库,达到更新程序的目的;
  • 缺点:程序启动时刻需要加载动态库,会增加启动耗时;依赖外部资源。

三、DYLD加载流程分析

什么是dyld?

dyld(the dynamic link editor)是苹果的动态链接器,苹果操作系统的重要 组成部分,在app被编绎打包成可执行文件格式的Mach-O文件后,交由dyld负责加载和链接。

dyld的源码是开源的,可以通过以下链接下载: opensource.apple.com/tarballs/dy…

1.APP启动的起始点

回到课程引入的demo,在load方法打下断点,运行,控制台输入“bt”打印调用堆栈

image.png

(1)从堆栈信息可以知道_dyld_start即为APP启动的起始点,打开dyld“dyld-852.2”源码,全局搜索“_dyld_start”

image.png

(2)_dyld_start是汇编实现的,内部调用了dyldbootrap:start函数,全局搜索“dyldbootstrap”找到命名作用空间,再搜索start函数

image.png

分析start函数的实现可以发现,其回调调用了dyld::main函数,其中macho_header为Mach-O头部,而验证了dyld加载的文件就是Mach-O类型的可执行文件。

(3)进入dyld::_main实现,发现实现代码特别长,主要做了以下九件事

image.png

第一步:环境变量配置,根据环境变量设置相应的值,以及获取当前的运行架构

image.png

第二步:检查是否开启共享缓存,并将共享缓存映射到共享区域,如UIKit、CoreFoundation等

image.png

第三步:主程序的初始化,加载可执行文件,为主程序实例化一个ImageLoader对象

image.png

第四步:插入动态库,遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载所有的动态库

image.png

第五步:链接主程序

image.png

第六步:链接动态库

image.png

第七步:弱符号绑定

image.png

第八步:执行初始化方法

image.png

第九步:寻找主程序入口---main函数

image.png

dyld::_main执行流程总结

image.png

(4)从dyld::_main的主流程我们还未能明确dyld是如何加载Mach-O文件的,以及未发现课堂引入例子中的问题的调用,下面我们重点分析一下第三步和第八步

第三步:sMainExecutable表示主程序变量,查看其赋值,是通过instantiateFromLoadedImage方法初始化主程序,查看其源码实现,创建了一个ImageLoader实例对象,通过instantiateMainExecutable创建

image.png

查看instantiateMainExecutable的实现,其作用是为主可执行文件创建镜像,返回一个ImageLoader类型的image对象,即主程序;其中sniffLoadCommands函数是获取Mach-O类型的Load Command并进行各种校验

image.png

第八步:执行初始化方法initializeMainExecutable函数,查看其实现,发现调用了runInitializers

image.png

全局搜索“runInitializers(const”找到runInitializers源码实现,其进一步调用了processInitializers函数

image.png

可以看到processInitializers调用了recursiveInitialization,查看注释的意思,镜像列表中的所有镜像调用递归实例化方法,以建立未初始化的向上依赖关系的列表

image.png

全局搜索“recursiveInitialization(const”,查看其源码实现,通过注释我们猜测并分析notifySingle和doInitialization函数

image.png

来到notifySingle函数,其关键调用sNotifyObjCInit

image.png

全局搜索sNotifyObjCInit,发现没有实现,只有在registerObjCNotifiers函数中有赋值,说明它是一个回调函数

image.png

再来到registerObjCNotifiers的实现,发现在_dyld_objc_notify_register调用了它,全局搜索_dyld_objc_notify_register,发现没有地方调用它

image.png

这里的_dyld_objc_notify_register其实是在libobjc中调用的,下载一份libobjc的源码,下载地址为:opensource.apple.com/tarballs/ob… 。打开libobjc“objc4-818.2”源码中搜索“_dyld_objc_notify_register”

image.png

在libobjc中发现_objc_init中调用了_dyld_objc_notify_register,传入了map_images、load_images、unmap_image三个参数

image.png

结合registerObjCNotifiers的实现及调用链路可以知道

sNotifyObjcInit == load_images

在libobjc中全局搜索load_images

image.png

来到call_load_methods函数的实现

image.png

进入call_class_loads函数实现,可以很明显看到,这里循环调用了所有类的load方法

image.png

通过上面的分析,我们知道load_images调用了所有类的load,以上源码分析过程正好对应课堂引入demo的堆栈调用顺序,至此解释了为什么load方法为什么会在main函数之前调用了

image.png

load方法调用链路总结

image.png

下面再来分析一下doInitialization函数,回到dyld源码,来到doInitialization的实现,它主要调用了doImageInit和doModInitFunctions函数

image.png

来到doImageInit源码实现,其核心主要是for循环加载方法的调用,这里需要注意的一点是,libSystem的初始化必须先运行.这里会调用libSysteam中的相关方法,我们可以下载libSysteam源码分析(后面分析),下载地址:opensource.apple.com/tarballs/Li…

image.png

查看doModInitFunctions实现,加载了所有的C++文件

image.png

回到课堂引入demo,在C++方法mideaCXXFun打上断点,bt打印堆栈信息,由此我们知道了C++的函数为什么会自动调用了,即doModInitFunctions。

image.png

通过前面的分析,我们知道了load方法和C++方法的调用逻辑。在分析load方法的调用时,我们发现是由_objc_init 调用_dyld_objc_notify_register给dyld注释了一个回调函数load_images,在dyld中调用load_images的后续调用链路中调用load方法的,那么_objc_init是在何时调用的呢?在dyld、libobjc、libSysteam源码中全搜索都没有发现其调用,这让我们怎么继续呢?

image.png

回到课堂引入demo,打上“_objc_init”的符号断点,运行卡住断点后bt打印堆栈信息可以发现_objc_init的调用链路如下图红框所示:

image.png

来到libSysteam源码,全局搜索libSystem_initializer函数。发现其确实调用了libdispatch_init函数。

image.png

下载libdispatch源码:opensource.apple.com/tarballs/li… 全局搜索libdispatch_init。在_os_object_init函数中调用了_objc_init

image.png

查看_os_object_init源码或搜索_objc_init函数.终于我们在libdispatch源码中找到了_objc_init的调用.

image.png

综合上述分析,从_objc_init调用_dyld_objc_notify_register的load_images,到sNotifySingle的sNotifyObjCInit(==load_images)的调用形成一个闭环。总结一下_objc_init的调用链路:

image.png

来到第九步:寻找main函数,最终dyld会寻找入口函数main,如果项目中没有main函数的话会报错

image.png

dyld加载应用启动流程总结

image.png

此图源自月月大神的博客:www.jianshu.com/p/db765ff4e…

四、dyld与objc的关联

从_dyld_objc_notify_register函数到registerObjCNotifiers调用我们可以发现如下的对应关系

image.png

前面我们已经分析过了sNotifyObjCInit(load_images)的调用逻辑,下面我们看一下sNotifyObjCMapped(map_images)。dyld源码中搜索sNotifyObjCMapped,发现在notifyBatchPartial函数中调用了它

image.png

搜索notifyBatchPartial函数找到它的调用,发现在registerObjCNotifiers中调用了它,并且之后调用了sNotifyObjCInit(load_images)

image.png

结论:map_images是先于load_images调用,即先map_images ,再load_images。结合前面分析的dyld加载流程,可以知道dyld的关联如下图所示

image.png

此图源自月月大神的博客:www.jianshu.com/p/db765ff4e…

回到_objc_init函数,我们来看一下它做了哪些事情

image.png

  • 1.初始化环境变量,并读取影响运行时的环境变量;
  • 2.关于线程key的绑定,主要是本地线程池的初始化以及析构;
  • 3.运行系统级别的C++静态构造函数,在dyld调用我们自己实现的C++函数(静态构造函数)之前,libststem调用_objc_init,即"系统级别的C++构造函数先于自定义的C++构造函数运行";
  • 4.运行时的初始化,包括分类的初始化和类的初始化;
  • 5.初始化libobjc的异常处理系统;
  • 6.缓存的初始化;
  • 7.启动回调机制;
  • 8.dyld注册。

注册异常回调句柄

image.png

查看环境变量:environ_init(),我们可以在终端输入“export OBJC_HELP=1”

image.png

Xcode所有环境变量对照表:

image.png

在Xcode项目中设置环境变量,以OBJC_PRINT_LOAD_METHODS为例,回到课堂引入demo,设置后运行起来即可在控制台打印实现了load方法的类和分类

image.png

五、由dyld加载应用启动流程引发的思考

  • 1、启动时刻需要加载动态库,动态库过多会增加启动耗时,项目动态库的数量要控制一下,苹果建议是6个;如果动态库太多,可以考虑合并动态库;
  • 2、启动时刻会遍历所有实现了load方法的类并执行load方法,增加启动耗时;可以考虑将需要在load方法中实现的代码放到initialize方法中,这个方法会在第一次调用该类的方法时执行一次。
  • 3、所有的C++函数都会在启动时刻自动执行,所以项目中应该尽量少写C++函数,这也会增加启动耗时;
  • 4、苹果的Open Source为我们开源了libobjc、libdispatch、libsysteam和dyld的源码等等,可以阅读他们了解iOS底层的实现逻辑。

image.png