我们都知道APP的入口函数是main(),而在main()函数调用之前,APP的加载过程是怎样的呢?接下来我们一起来分析APP的加载流程。
一. 准备工作
由于load()
比main()
调用更早,因此我们创建一个工程,在控制器中写一个load()
函数,并断点运行,如下图:
运行起来之后,可以清晰的看到比较详细的函数调用顺序,从_dyld_start()
到dyld:notifySingle()
,频率出现最多的就是这个dyld,那么dyld是什么?它在做什么?
简单来说dyld是一个动态链接器,用来加载所有的库和可执行文件。接下来我们将通过对dyld源码分析,去追踪dyld到底做了什么?
二. dyld加载流程分析
1. 首先下载dyld源码。
2. 打开dyld源码工程,根据上图dyldbootstrap::start
为关键字搜索dyldbootstrap
中调用的start()
,如下图:
3. 进入dyld的start
函数
其中rebaseDyld()
分析如下:
4. 进入dyld的main
函数
注:因为
dyld::main()
函数代码比较多,以下会分段介绍,也会介绍相对来说比较重要的函数。
4.1 内核检测
4.2 获取main执行文件的cdHash缓存区
4.3 获取CPU信息
// 获取CPU信息
getHostInfo(mainExecutableMH, mainExecutableSlide);
4.4 设置MachHeader和内存偏移量
// 设置MachHeader和内存偏移量Slide
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
sMainExecutableSlide = mainExecutableSlide;
4.5 设置上下文
// 设置上下文,保存信息
setContext(mainExecutableMH, argc, argv, envp, apple);
4.6 配置进程限制
4.7 检测环境变量
4.8 打印环境配置信息
// 打印环境配置信息
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
此处可以自己定义环境变量配置,回到刚才创建的新工程中,在Edit Scheme
-> Run
-> Arguments
-> Environment Variables
添加两个参数 DYLD_PRINT_OPTS
和 DYLD_PRINT_ENV
,并设置测试value值,如下:
运行程序,可看到如下打印信息:
4.9 加载共享缓存(如果没有共享缓存,iOS将无法运行)
主要函数mapSharedCache()
如下:
4.10 dyld
配置
(1) dyld3
(闭包模式)
iOS11版本之后,引入dyld3闭包模式(ClosureMode):加载速度更快,效率更高。
开始执行闭包模式
判断是否开启了闭包模式
启动闭包模式加载
其中launchWithClosure
加载闭包,会把后面说到的dyld2
的大部分流程都封装到launchWithClosure()
这个函数里面了,这里不再细说launchWithClosure
,因为在接下来的dyld2
(非闭包)中会详细解释整个dyld加载的流程,也就是launchWithClosure
实现过程。
(2) dyld2
(非闭包模式)
开始执行闭包模式
把dyld加入到UUID列表
// 把dyld加入到UUID列表
addDyldImageToUUIDList();
配置缓存代理
4.11 创建主程序的Image
开始创建主程序的Image,通过instantiateFromLoadedImage()
,调用instantiateMainExecutable()
,实例化具体的Image类,最后生成的对象,设置到gLinkContext
中。
4.12 设置动态库的版本
// 加载完共享缓存,设置动态库的版本
checkVersionedPaths();
4.13 加载插入的动态库
// 加载插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
oadInsertedDylib(*lib);
}
4.14 链接主程序和动态库
- 其中的
link()
函数,函数体调用了image->link()
,函数具体如下:
- 判断是否需要重新加载所有的Image
4.15 绑定主程序和动态库
4.16 初始化主程序
根据Demo上的堆栈信息,如下:
终于看到熟悉的函数了,那么dyld加载流程也快结束了。
根据堆栈信息,获取函数调用层级关系。
// 初始化主程序
initializeMainExecutable();
- 查找
runInitializers()
:dyld::initializeMainExecutable()
->ImageLoader::runInitializers()
- 查找
processInitializers()
:ImageLoader::runInitializers()
->ImageLoader::processInitializers()
- 查找
recursiveInitialization()
:ImageLoader::processInitializers()
->ImageLoader::recursiveInitialization()
- 查找
notifySingle()
:ImageLoader::recursiveInitialization()
->dyld::notifySingle()
- 查找
load_images()
:在dyld::notifySingle()
中并没有找到load_images()
,但是找到了sNotifyObjCInit()
,该字段是objc函数回调。在dyld::notifySingle()
中执行了这个回调,那就需要追溯到谁去注册的这个回调了。 - 全局查找
sNotifyObjCInit()
赋值的地方。在registerObjCNotifiers()
中赋值,如下:
- 全局查找
registerObjCNotifiers
,在_dyld_objc_notify_register()
中调用,且第二个参数是我们需要的。如下:
- 全局查找
_dyld_objc_notify_register()
,并没有在dyld源码库里找到,此时需要在源工程中,打符号断点_dyld_objc_notify_register
,重新编译执行,可以看到是_objc_init()
调用了。此时只能去查找objc源码了。 -
objc
源码分析,在objc-os.mm
文件中找到_objc_init()
函数,其中注册了_dyld_objc_notify_register
回调。
其中第二个参数就是load_images()
,在load_images()
中也找到了call_load_methods()
。
此时初始化程序还未执行完成,回到之前的 ImageLoader::recursiveInitialization()
方法中。
- 执行
this->doInitialization()
函数
- 发送通知,初始化主程序完成。
4.17 进入主程序
// 通知此进程将要进入程序main()
notifyMonitoringDyldMain();
到此,start()
-> main()
,全部执行完毕。
三. 总结
- dyld(动态链接器):是苹果操作系统一个重要组成部分,加载所有的库和可执行文件。
- dyld加载流程:
- 从
_dyld_start()
开始 ->dyldbootstrap::start()
- 进入dyld的
main()
- 检测内核,配置重定向信息:
rebase_dyld()
- 加载共享缓存
-
dyld2
/dyld3
(闭包模式)- 实例化主程序
- 加载动态链接库 (主程序和动态库的image都会加载allImage里面:
loadAllImage
,主程序在第0
位置) - 链接主程序、动态库、绑定符号(非懒加载、弱符号)等
-
最关键
:初始化方法initializeMainExecutable()
ImageLoader::runInitializers()
ImageLoader::processInitializers()
ImageLoader::processInitializers()
ImageLoader::recursiveInitialization()
-
dyld::notifySingle()
- 此函数执行一个回调
_dyld_objc_notify_register()
- 通过断点调试:此回调是
_objc_init()
初始化时赋值的一个函数load_images()
,里面执行了call_load_methods()
函数,其作用是循环调用各个类的方法。
- 此函数执行一个回调
-
doModInitFunctions()
函数:内部会调用全局C++对象的构造函数__attribute__((constructor))
的C函数
- 返回主程序入口,执行
main
函数
- 从