实际开发中,大部分人都只知道main是程序的入口。但是app在启动前,具体做了哪些事情,如何保证进入main函数时,所有资源都准备好了?+(void)load函数为何能帮你把一些自定义事项在启动前就处理好?
如果你也有这些疑问,那本节,我们一起探索应用程序的整个启动加载过程。
1. 检查main、load、C++(constructor) 的执行顺序
2. 静态库与动态库
3. app启动加载过程
准备工作
- 可编译的
objc4-781源码: https://www.jianshu.com/p/45dc31d91000dyld-750.6: https://opensource.apple.com/tarballs/dyld/libdispatch-1173.40.5: https://opensource.apple.com/tarballs/libdispatch/Libsystem-1281: https://opensource.apple.com/tarballs/Libsystem/
1. main、load、C++ 的执行顺序
- 测试代码:
__attribute__((constructor)) void htFunc() {
printf("%s \n",__func__);
}
@interface HTPerson : NSObject
@end
@implementation HTPerson
+ (void)load {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%s",__func__);
}
return 0;
}
- 打印顺序:
load->c++(constructor)->main
image.png
main函数作为程序入口,为什么是最后执行呢?
带着这个疑问,我们往下学习。
2. 静态库与动态库
代码库有静态库和动态库两种,在开始探索app启动流程前,我们先了解两者的区别。
2.1 静态库:
静态编译的库,在编译时就将整个函数库的所有数据都整合进目标代码中。尾缀有.a、.lib、.framework等。
- 优点:
模块化,分工合作,提高了代码的复用和核心技术的保密程度 - 缺点: 会
加大包的体积。如果静态函数库被改变,程序必须重新编译。
2.2 动态库:
编译时不会将函数库编译进目标代码中,只有程序执行到相关函数时,才调用函数库的相应函数。尾缀有.tbd、.so、.framework等
- 优点: 可执行文件
体积小,多个应用程序共享内存中同一份库文件,节省内存资源,支持实时模块升级。
苹果的
动态库支持所有APP共享内存(如UIKit),但APP的动态库是写入app main bundle根目录中,运行在沙盒中,只支持当前APP内共享内存。(iOS8后App Extension功能支持主app和插件之间共享动态库)
3. App加载过程
我们直观感受的App加载过程是:源文件(.h .m .cpp)-> 预编译(词法语法分析) -> 编译(载入静态库) -> 汇编 -> 链接(关联动态库) -> 生成可执行文件(mach-o)
作为程序员,我们知道代码是
“死”的,只有当触发启动,按照我们设计好的流程一步步执行,才能让程序“活”起来。
在程序启动过程中,当系统内核把资源准备好后,dyld动态链接器就承担着管理者的角色:
配置应用环境->初始化主程序->加载共享缓存->加载动态库->链接主程序->链接动态库->弱符号绑定->执行初始化->调用main函数。
到了main函数后,就交给程序员们自由发挥了。
dyld全称the dynamic link editor,动态链接器。是苹果操作系统的一个重要组成部分。在iOS/Mac OSX系统中,仅有很少量的进程只需要内核就能完成加载,基本上所有进程都是动态链接的,所以mach-o镜像文件中会有很多外部库和符号的引用,但这些引用并不能直接用,在启动时还需要通过这些引用进行内容的填补,这个填补工作就是dyld动态链接器来完成的,也就是符号绑定。dyld动态链接器在系统中是以一个用户态的可执行文件存在,一般应用程序会在Mach-o文件部分指定一个LC_KIAD_DYLINKER的加载命令,此加载命令指定了dyld的路径,通常它的默认值是/usr/lib/dyld。系统内核在加载Mack-o文件时,都需要用dyld(位于/usr/lib/dyld)程序进行链接。
共享缓存机制
在iOS生态中,
每个程序都会用到大量的系统库,但如果我们每个程序运行时,都独立去加载其依赖的相关动态库,势必会造成运行缓慢。为了优化启动速度和程序性能,共享缓存机制应运而生。所有默认的动态链接库被合并成一个大的缓存文件,按不同架构分别保存。
本节主要是梳理和验证APP启动的完整流程。具体内部细节和使用法决,后续在其他文章中进行拓展。
- 我们在
load函数内部打断点,bt打印堆栈信息
image.png
从
bt打印的堆栈信息中可以看到,每一步都是dyld在进行调用的
-
堆栈信息中展示了APP启动前的完整流程。接下来我们就沿着这个流程,从源码中寻找答案。
启动dyld
第一步:执行dyld中的_dyld_start
我们打开dyld源码,全局搜索_dyld_start,找到入口:
image.png
- 我们从汇编代码中看到调用了
dyldbootstrap::start,与我们的第二步完全吻合。
第二步:执行dyldbootstrap::start
- 全局搜索
dyldbootstrap,发现是个命名空间,折叠内部函数,找到start函数:
image.png
打开start函数,发现最后执行了dyld::_main函数,这也与我们第三步完全吻合
image.png
第三步:执行_main函数
进入main函数,发现有600多行😂 ,在这里,我们可以梳理出APP启动的完整流程:
image.png
3.1 设置运行环境
- 设置运行参数、环境变量,获取当前运行框架
image.png
3.2 加载共享缓存
-
checkSharedRegionDisable检查共享缓存是否禁用后,调用mapSharedCache加载共享缓存。
image.png
3.3 实例化主程序
- 将
主程序Mach-O加载进内存,返回一个ImageLoader类型的image对象,即主程序
image.png
3.4 加载插入的动态库
- 遍历
DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载库
image.png
3.5 链接主程序
image.png
3.6 链接插入的动态库
image.png
3.7 执行弱符号绑定
image.png
3.8 执行初始化方法
image.png
- 进入
initializeMainExecutable函数
image.png
-
发现都是调用
ImageLoader对象的runInitializers方法来初始化dylib和主程序 -
全局搜索
runInitializers,在ImageLoader.cpp文件中找到实现函数。
image.png
- 核心代码为
processInitializers函数的调用,进入:
image.png
-
recursiveInitialization是ImageLoader对象的调用方法,全局搜索:
image.png
-
递归完成了所有对象的初始化,并将镜像初始化进度实时告知外部关联对象。
3.9 寻找main入口
image.png
以上就是完整的app启动流程。
这里对3.8 执行初始化方法 最后一步的2个内容进行继续探究:
-
notifySingle如何告知外部 -
doInitialization初始化
1. notifySingle如何告知外部
-
全局搜索
notifySingle:
image.png
-
核心代码:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader()),我们全局搜索sNotifyObjCInit,发现没有找到实现,但是有赋值操作:
image.png
- 搜索
registerObjCNotifiers在哪里被调用:
image.png
- 发现在
_dyld_objc_notify_register中调用。而dyld_objc需要在libobjc源码中搜索。 - 我们打开
objc4源码,搜索_dyld_objc_notify_register(
image.png
- 发现在
_objc_init方法中调用了_dyld_objc_notify_register方法,并传入了入参,所以sNotifyObjCInit的赋值是objc传入的load_images函数指针。因为入参是指针,所以notifySingle是一个回调函数。
回调函数:
通过函数指针调用的函数
把函数指针(地址)作为参数传递给另一个函数,当该指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
我们探索一下load_images函数内部:
load_images函数
- 进入
load_images函数内部,核心代码为call_load_methods的调用
image.png
- 进入
call_load_methods函数,核心代码循环调用call_class_loads函数
image.png
- 进入
call_class_loads函数内部,此处明确了load方法的调用。
image.png
- 明确
+load方法的加载时机;
- 明确只有
+load这个名称才有效(因为sel已固定,系统只检查load这个方法名)
对比在+load函数断点处打印的堆栈信息,与我们源码分析过程完全吻合。
image.png
notifySingle从dyld跨库到objc,调用了load_images函数,调用了所有+load函数
HTPerson类的Load函数被调用的完整流程:
- 程序启动
_dyld_start
-> 调用dyldbootstrap::start函数 -> 调用dyld::_main函数
-> 主程序初始化initializeMainExecutable-> 镜像初始化ImageLoader::runInitializers
-> 进程初始化ImageLoader::processInitializers-> 递归初始化ImageLoader::recursiveInitialization
-> 消息发送dyld::notifySingle-> 跨到objc源码库调用load_images-> 调用+load方法
但是,_objc_init什么时候调用的呢? 我们继续往下探索:
2. doInitialization初始化
- 回到3.8步骤,我们理清楚了
notifySingle的消息流程(调用回调函数),接下来看doInitialization初始化动作:
image.png
- 进入
doInitialization:
image.png
发现有doImageInit和doModInitFunctions2个初始化操作
-
doImageInit函数,for循环实现镜像的初始化(macho内获取地址和偏移值,拿到初始化函数),libSystem系统库的初始化优先级较高。
image.png
-
doModInitFunctions函数: 该函数内实现了所有Cxx文件:
image.png
在测试代码的c++构造函数constructor处加入断点,bt打印堆栈信息检验,确实是在doModInitFunctions函数内完成了实现。
image.png
探索_objc_init调用时机
在objc4源码中搜索_objc_init,加入断点,运行测试代码。
image.png
- 发现也是在
doModInitFunctions函数后,调用了libSystem库的initializer方法。
image.png
验证流程:
- 打开
libSystem源代码,搜索libSystem_initializer:image.png
- 进入
libdispatch_init,发现什么在libdispatch.dylib库中实现。image.png
打开
libdispatch源码,搜索libdispatch_init:
image.png
发现调用了
os_object_init,搜索_os_object_init:image.png
在此处调用了
_objc_init。
_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)
此刻,回到文初的问题,main、load、C++ 的执行顺序?是否已非常清晰。
-
load: 在 3.8 执行初始化方法的recursiveInitialization函数中,第一次调用notifySingle时完成了所有+load的调用。 -
c++: 在第一次调用notifySingle函数之后,调用doInitialization函数中,完成了所有c++函数的调用和所有库的初始化 -
main: 在 3.9 寻找main入口后,开始调用main函数。
强烈建议阅读以下官方资源:
- 快速熟悉Mach-O结构(后续有变动)
- dyld如何将mach-o信息映射到内存中
- app启动流程(旧版)和优化建议
- 介绍dyld历史,引出dyld3(围绕性能、安全、占用资源进行优化)
- 介绍App Launch工具,优化启动时间
本文仅简单记录dyld的大致启动流程,部分细节并未展开拓展。源码的探索之旅继续进行...

image.png
image.png
image.png
image.png











网友评论