实际开发中,大部分人都只知道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
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
打印堆栈信息
从
bt
打印的堆栈信息
中可以看到,每一步都是dyld
在进行调用
的
-
堆栈信息
中展示了APP启动
前的完整流程
。接下来我们就沿着
这个流程
,从源码
中寻找答案。
启动dyld
第一步
:执行dyld
中的_dyld_start
我们打开dyld
源码,全局搜索_dyld_start
,找到入口:
- 我们从汇编代码中看到调用了
dyldbootstrap::start
,与我们的第二步完全吻合。
第二步
:执行dyldbootstrap::start
- 全局搜索
dyldbootstrap
,发现是个命名空间,折叠内部函数,找到start
函数:
打开start
函数,发现最后执行了dyld::_main
函数,这也与我们第三步完全吻合
第三步
:执行_main
函数
进入main
函数,发现有600多行😂 ,在这里,我们可以梳理出APP启动的完整流程:
3.1 设置运行环境
- 设置运行参数、环境变量,获取当前运行框架
3.2 加载共享缓存
-
checkSharedRegionDisable
检查共享缓存是否禁用后,调用mapSharedCache
加载共享缓存。
3.3 实例化主程序
- 将
主程序Mach-O
加载进内存,返回一个ImageLoader
类型的image
对象,即主程序
3.4 加载插入的动态库
- 遍历
DYLD_INSERT_LIBRARIES
环境变量,调用loadInsertedDylib
加载库
3.5 链接主程序
3.6 链接插入的动态库
3.7 执行弱符号绑定
3.8 执行初始化方法
- 进入
initializeMainExecutable
函数
发现都是调用
ImageLoader
对象的runInitializers
方法来初始化dylib
和主程序
全局搜索
runInitializers
,在ImageLoader.cpp
文件中找到实现函数
。
- 核心代码为
processInitializers
函数的调用,进入:
-
recursiveInitialization
是ImageLoader
对象的调用方法,全局搜索:
-
递归
完成了所有对象的初始化
,并将镜像初始化进度
实时告知
外部关联对象。
3.9 寻找main入口
以上就是完整的app启动流程。
这里对3.8 执行初始化方法 最后一步的2个内容进行继续探究:
-
notifySingle
如何告知外部 -
doInitialization
初始化
1. notifySingle如何告知外部
-
全局搜索
notifySingle
:
核心代码:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())
,我们全局搜索sNotifyObjCInit
,发现没有找到实现,但是有赋值操作
:
- 搜索
registerObjCNotifiers
在哪里被调用:
- 发现在
_dyld_objc_notify_register
中调用。而dyld_objc
需要在libobjc
源码中搜索。 - 我们打开
objc4
源码,搜索_dyld_objc_notify_register(
- 发现在
_objc_init
方法中调用了_dyld_objc_notify_register
方法,并传入了入参,所以sNotifyObjCInit
的赋值是objc
传入的load_images
函数指针
。因为入参是指针
,所以notifySingle
是一个回调函数
。
回调函数:
通过函数指针调用的函数
把函数指针(地址)
作为参数传递
给另一个函数,当该指针被
用来调用
其所指向的函数
时,我们就说这是回调函数
。
回调函数不是
由该函数的实现方直接调用
,而是在特定的事件或条件
发生时由另外的一方调用
的,用于对该事件或条件进行响应
。
我们探索一下load_images
函数内部:
load_images函数
- 进入
load_images
函数内部,核心代码为call_load_methods
的调用
- 进入
call_load_methods
函数,核心代码循环调用call_class_loads
函数
- 进入
call_class_loads
函数内部,此处明确了load
方法的调用。
- 明确
+load
方法的加载时机;
- 明确只有
+load
这个名称才有效(因为sel
已固定,系统只检查load
这个方法名)
对比在+load
函数断点处打印的堆栈信息
,与我们源码分析过程
完全吻合
。
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
初始化动作:
- 进入
doInitialization
:
发现有doImageInit
和doModInitFunctions
2个初始化操作
-
doImageInit
函数,for循环
实现镜像的初始化(macho内获取地址和偏移值,拿到初始化函数),libSystem
系统库的初始化优先级较高。
-
doModInitFunctions
函数: 该函数内实现了所有Cxx
文件:
在测试代码的c++
构造函数constructor
处加入断点
,bt
打印堆栈信息检验
,确实是在doModInitFunctions
函数内完成了实现。
探索_objc_init
调用时机
在objc4
源码中搜索_objc_init
,加入断点
,运行测试代码。
- 发现也是在
doModInitFunctions
函数后,调用了libSystem
库的initializer
方法。
验证流程:
- 打开
libSystem
源代码,搜索libSystem_initializer
:
- 进入
libdispatch_init
,发现什么在libdispatch.dylib库中实现。
打开
libdispatch
源码,搜索libdispatch_init
:
发现调用了
os_object_init
,搜索_os_object_init
:在此处调用了
_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的大致启动流程,部分细节并未展开拓展。源码的探索之旅继续进行...