应用程序加载
方法等是如何加载到内存中的呢,或者说类的加载都做了什么?在此之前,我们先探索 APP 从响应用户点击到完全启动的过程 即 应用程序加载 都做了什么事情。
首先我们准备一个 iOS 的 APP 工程,ViewController.m 中添加 load 方法,main.m 中添加一个 C++ 的 方法,代码如下图:
执行流程: load –> C++ –> main() .
为何 load 和 C++ 方法在函数之前呢?
在此之前,我们要先大概回顾下编译流程。
一、编译过程
/* 书籍推荐:《程序员的自我修养》*/
.h .m 等源文件的预编译 –> 编译 –> 汇编 –> 链接 –> 可执行文件
1、动静态库
静态库:.a .lib 等文件 - 编译器链接
在链接阶段,将 汇编生成的一些目标文件,与引进的一些库链接在一起,打包成可执行文件,也称为静态链接,链接的是静态库。
但静态库会同时生成多份,对内存和性能消耗比较大。
动态库:.framework .so 等文件 - 运行期链接
动态库,在编译时并不会编译到目标代码中,而是在程序被载入的时候,把相同的库用一份共享库的实例将其将在进去。
同样的库只存在一份大大节省了内存 (增量更新),共享内存节约了资源,通过更新动态库达到更新程序的效果;同时减小了整个 APP 打包后的大小。例如:UIKint UIFoundation CoreFoundation libobjc 等。
这些动态库如何加载呢?通过 dyld 链接器进行链接。
2、dyld 动态链接器
app 启动 –> 下层的很多动静态库:镜像文件 (images) –> 由 dyld 进行处理:从加载的内存中读出来,读到相应的表中;然后加载主程序 –> 之后进行相应的 link –> 库的初始化 (例 runtime 的 _objc_init) –> 这些必然要通过 dyld 来处理的,下面进行 dyld 的探究。
二、源码分析
1、主流程
dyld-750.6 源码下载,源码文件很多,我们如何入手呢?
如上图,我们已知 load 方法首先执行,可打印下堆栈信息,可知,在此之前最初走了 _dyld_start .
全局搜索 dyld_start,源码 是通过汇编编写:
1、全局搜索 dyldbootstrap 方法
命名空间包含整个代码文件,我们找到 start,从上面打印的堆栈信息中也可得知:dyldbootstrap::start() 之后走 dyld::_main()。
2、dyld::_main():
代码有点长 6192~6828, _main 函数返回值 是 return result,搜索 result,找到下面几处赋值:
我们直接从 6780 行开始读代码,可知 result = (uintptr_t)sMainExecutable->getEntryFromLC_xxx. 可执行主程序。(第 6796 行是个特殊特性的判断,我们暂不管它)
2.1)文件内搜索 sMainExecutable:
由上源码,可知 instantiateFromLoadedImage 方法主要做的是镜像文件加载,继续进入方法:instantiateMainExecutable:可以看到是一些 command 处理。我们打开工具 machOView,将可执行文件拖入,如下图:
简单来讲,即:instantiateFromLoadedImage 过程主要是做了:初始化创建了一个 imageLoader,加在了 image 里面,然后返回了一个主程序 sMainExecutable.
dyld 做了什么 (源码这里不再贴过来):
- 环境变量配置 -
- 共享缓存 -
- 主程序的初始化 -
- 插入动态库 - .
- link 主程序 -
- link 动态库
- dyld 的 main() 函数
version、platform、path 上下文 context 等 –> mapSharedCache() –> 6577 行 sMainExecutable –> 6641 行 for 循环的 - loadInsertedDylib() –> 6659 行 link(): sMainExecutable/images –> run all initializers: initializeMainExecutable**()** –> dyld 的 main: notifyMonitoringDyldMain() .
2、initializeMainExecutable() 初始化
initializeMainExecutable() 源码:
1 |
|
1、递归进行 runInitializers() .
2、跳到 processInitializers():
3、搜索 recursiveInitialization():
**3.1)搜索 notifySingle()**:
查找 sNotifyObjCInit:
sNotifyObjCInit 是通过函数 registerObjCNotifiers() 传过来的,
_dyld_objc_notify_register 在 dyld 源码中找不到,我们去 objc 源码中查找试试:
找到了 _objc_init 里面去了,那我们要探索的 initial 在哪呢?
通过 3.1 我们知道 notifySingle 回调,下面继续探索初始化流程。
3.2)回到 recursiveInitialization() 代码
1598 行:
跳转 doInitialization() :
doImageInit():
通过 dyld 源码流程分析,可对应下面堆栈信息的执行流程:
下载 libsystem 源码 - libsyscall_initializer() 初始化部分源码:
1 |
|
从上源码可以看到 _dyld_initializer() –> libdispatch_init() .
下载 libdispatch 源码 - libdispatch_init() 部分源码:
1 |
|
_os_onjc_init(): –> _objc_init()
由此,整个执行流程实现了一个闭环 (下图堆栈信息来自 objc 源码工程):
总结:应用程序整个加载流程:
dyld_start –> …… –> libsystem: libsyscall_initializer() –> _dyld_initializer() –> libdispatch_init() –> libDispatch: _os_onjc_init() –> libobjc.A.dylib: _objc_init() –> _dyld_objc_notify_register(参数 1, 参数 2) 回调函数
notifySingle() –> sNotifyObjCInit() –> registerObjCNotifiers() : 参数 2
3、下面继续探索
我们通过文章顶部已知,方法的执行顺序是 load –> C++ –> main 下面对其原因进行简单探究。
1)load 方法调用
_objc_init() 源码 中 load_images:
跳转 call_load_methods():
调 call_class_loads():
2)Cxx 方法调用
doInitialization 源码中 doModInitFunctions(): 全部 Cxx 函数调用
通过堆栈信息验证:
3)main 函数
如下,dyldbootstrap::start() 执行完毕后,跳转到 寄存器 rax 的位置,rax 即 main 函数:
tip: main 函数作为入口函数,它是一个写定的函数,它的名字是固定的 main。
文章上面 dyld_start 源码中可知:
汇编源码中:call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue) –> LC_MAIN case, set up stack for call to main()
以上。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!