本文主要是分析main函数之前,底层做了什么 -- dyld
的加载流程
例子
-
新建一个项目,在
ViewController
中重写laod
方法,然后再main
函数中添加一个C++方法
,查看它们的打印顺序
-
运行程序,查看打印结果,可以看出执行顺序是
load --> C++方法 --> main函数
根据打印结果,我们来探索程序在mian函数之前做了什么
编译过程
-
源文件
:载入.h,.m,.cpp等文件 -
预处理
:替换宏,删除注释,展开头文件,生成.i
文件 -
编译
:将.i
文件转化为汇编语言,生成.s
文件 -
汇编
:将汇编文件.s
转化成机器码文件,生成.o
文件 -
链接
:对.o
文件中引用其他库的地方进行引用链接,生成可执行文件
静态库 和 动态库
静态库
静态库
:在链接阶段,会将汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。打包后的静态库就不会在改变,因为它是在编译时直接拷贝一份,复制到目标程序中
-
优点
:编译完成后,库文件实际上没有作用了,目标程序没有外部依赖,直接可以运行 -
缺点
:静态库会有两份,所有会导致目标程序的体积增大
,对内存、性能、速度消耗大
动态库
动态库
:程序在编译时并不会链接到目标程序
,目标程序只会动态存储指向动态库的引用
,在程序运行时才被载入
-
优点
:-
减少打包之后App的大小
:因为不需要拷贝到目标程序中,所有不会影响目标程序的体积,与静态库相比,减少了App的大小 -
共享内存,节约资源
:同一份库可以被多个程序使用 -
更新动态库,可以更新程序
:由于运行时才载入的特性,可以随时对库文件进行替换,而不需要重新编译代码
-
-
缺点
:动态载入会带来部分性能损失
,使用动态库也会使得程序依赖外部环境,如果环境中缺少了动态库,或者库版本不正确,会导致程序无法运行
dyld加载流程分析
什么是dyld
dyld
(the dynamic link editor)是苹果的动态链接器
,是苹果操作系统的重要组成部分,在app被编译打包成可执行文件(Mach-o
)后,交由dlyd负责链接,加载程序
App启动流程
- 在前往demo中,laod方法处加一个断点,通过
bt
堆栈信息查看app启动从哪里开始
,
从堆栈信息可以看出dyld
是从_dyld_start
开始,下载一份dyld源码进行分析
dyld::_main函数源码分析
-
在
dyld-750.6
源码中查找_dyld_start
(arm64架构中),我们发现是由汇编实现的,通过汇编注释发现会调用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
方法,是一个C++
方法
-
源码中搜索
dyldbootstrap
,找到命名作用空间,再在这个文件中查找start
方法,其核心是返回值调用了dyld
的main
函数,其中macho_header
是Mach-o
的头部,而dyld
加载的文件就是Mach-o
可执行文件,其由四部分组成:Mach-o头部
,Load Command
,section
,Other Data
,可以通过MachOView
查看可执行文件信息 -
dyld::_main
的源码实现,在_main函数中主要做了以下几件事:-
【第一步:
配置环境变量
】根据环境变量设置响应的值以及获取当前运行架构 -
【第二步:
共享缓存
】检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKit
,CoreFoundation
等框架
-
【第三步:
主程序初始化
】调用instantiateFromLoadedImage
函数实例化一个ImageLoader
对象
-
【第四步:
插入动态库
】遍历DYLD_INSERT_LIBRARIES
环境变量,调用loadInsertedDylib
加载
-
【第五步:
link主程序
】
-
【第六步:
link动态库
】
-
【第七步:弱符号绑定】
-
【第八步:执行初始化方法】
-
- 【第九步:寻找主程序入口(main函数)】从
Load Command
读取LC_MAIN
入口,如果没有,就读取LC_UNIXTHREAD
,然后就进入到熟悉的main函数
下面主要分析下【第三步】和【第八步】
【第三步:主程序初始化】
-
sMainExecutable
表示主程序变量,查看其赋值,是通过instantiateFromLoadedImage
方法初始化
-
进入
instantiateFromLoadedImage
源码,通过instantiateMainExecutable
方法创建一个ImageLoader
实例对象
-
进入
instantiateMainExecutable
源码,起作用是为可执行文件创建镜像,返回一个ImageLoader
类型的image对象,即主程序
,其中sniffLoadCommands
函数时获取Mach-o文件
的Load Command
的相关信息,并对其进行校验
第八步:执行初始化方法
-
进入
initializeMainExecutable
源码,该方法主要是循环遍历
,都会执行runInitializers
方法
-
全局搜索
runInitializers(
,其核心函数是processInitializers
函数的调用
-
进入
processInitializers
函数的源码实现,其中对镜像列表调用recursiveInitialization
函数进行递归实例化
-
全局搜索
recursiveInitialization(
函数,源码如下
在这里我们分为两部分探索:notifySingle
函数 和 doInitialization
函数
notifySingle函数
-
全局搜索
notifySingle(
,核心函数是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
-
全局搜索
sNotifyObjCInit
,发现没有找到实现,但是有赋值操作
-
搜索
registerObjCNotifiers
,发现在_dyld_objc_notify_register
进行了调用,注意:_dyld_objc_notify_register
的函数需要在libobjc
源码中搜索
-
在
objc4-781
源码中搜索_dyld_objc_notify_register
,发现在_objc_init
源码中调用了该方法,并传入了参数,所有sNotifyObjCInit
中的赋值就是objc
中的load_images
,而load_images
会调用所有+load
方法,综上所述--notifySingle
是一个回调函数
load函数加载
下面我们进入load_images
的源码,来证明load_images
中调用了所有的+laod
函数
-
在
objc
源码中找到_objc_init
源码实现,并进入load_images
的源码实现
-
进入
call_load_methods
源码实现,可以发现其核心是通过do-while
循环调用+laod
方法
-
进入
call_class_loads
源码,我们可以发现这里调用了+load
方法
-
所有
load_images
调用了所有的+load
函数,正好对应堆栈的打印信息
【总结】load
的源码调用链:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一个回调处理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
这时候我们发现没有找到_objc_init
的调用,那是因为忽略一个函数doInitialization
doInitialization函数
-
我们在
objc
的_objc_init
函数,发现走不通了,我们回到recursiveInitialization
递归函数的实现,发现我们忽略一个函数doInitialization
-
进入
doInitialization
源码
这里也需要分成两部分,一部分是doImageInit
函数,一部分是doModInitFunctions
函数
-
进入
doImageInit
源码实现,其核心主要是for循环加载方法的调用
,这里需要注意:libSystem
的初始化必须先运行
-
进入
doModInitFunctions
源码,这个方法中加载了所有的Cxx
文件
可以通过测试程序的堆栈信息来验证,在C++方法处加一个断点
`
走到这里,还是没有找到_objc_init的调用?怎么办呢?放弃吗?当然不行,我们还可以通过_objc_init加一个符号断点来查看调用_objc_init前的堆栈信息,
-
_objc_init
加一个符号断点
,运行程序,查看_objc_init断住后的堆栈信息
-
在
libsystem
中查找libSystem_initializer
-
根据前面的堆栈信息,我发现走的是
libSystem_initializer
中会调用libdispatch_init
函数,而这个函数的源码在libdispatch
开源库中,在libdispatch
中搜索libdispatch_init
-
进入
_os_object_init
源码实现,其中调用了_objc_init
函数
结合上面的分析,从初始化
_objc_init
注册的_dyld_objc_notify_register
的参数2,即load_images
,到sNotifySingle
-->sNotifyObjCInie=参数2
-- >sNotifyObjcInit()
调用,形成了一个闭环
所以可以简单的理解为sNotifySingle
在这里添加通知即addObserver
,_objc_init
中调用_dyld_objc_notify_register
相当于发送通知,即push
,而sNotifySingle
相当于通知的处理函数,即selector
【总结】:_objc_init的源码链:_dyld_start
--> dyldbootstrap::start
--> dyld::_main
--> dyld::initializeMainExecutable
--> ImageLoader::runInitializers
--> ImageLoader::processInitializers
--> ImageLoader::recursiveInitialization
--> doInitialization
--> libSystem_initializer
(libSystem.B.dylib) --> _os_object_init
(libdispatch.dylib)--> _objc_init
(libobjc.A.dylib)
第九步:寻找主入口函数
-
汇编调试,可以看到显示
+[ViewController load]
方法
-
继续执行,来到
kcFunc
的C++函数
-
点击
stepover
,继续往下,跑完了整个流程,会回到_dyld_start
,然后调用main()
函数,通过汇编完成main的参数赋值等操作
dyld
汇编源码实现
注意:
main
是写定的函数,写入内存,读取到dyld,如果修改了main函数的名称,会报错
所以,综上所述,最终dyld加载流程,如下图所示,图中也诠释了前文中的问题:为什么是load-->Cxx-->main
的调用顺序