iOS 进阶之路

OC底层原理十五:dyld 应用程序加载

2020-10-15  本文已影响0人  markhetao

OC底层原理 学习大纲

实际开发中,大部分人都只知道main是程序的入口。但是app在启动前,具体做了哪些事情,如何保证进入main函数时,所有资源都准备好了?+(void)load函数为何能帮你把一些自定义事项在启动前就处理好?

如果你也有这些疑问,那本节,我们一起探索应用程序的整个启动加载过程。

1. 检查main、load、C++(constructor) 的执行顺序
2. 静态库与动态库
3. app启动加载过程

准备工作

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;
}
image.png

带着这个疑问,我们往下学习。

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启动的完整流程。具体内部细节使用法决,后续在其他文章中进行拓展

image.png

bt打印的堆栈信息中可以看到,每一步都是dyld在进行调用

启动dyld

第一步:执行dyld中的_dyld_start

我们打开dyld源码,全局搜索_dyld_start,找到入口:

image.png

第二步:执行dyldbootstrap::start

image.png

打开start函数,发现最后执行了dyld::_main函数,这也与我们第三步完全吻合

image.png

第三步:执行_main函数

进入main函数,发现有600多行😂 ,在这里,我们可以梳理出APP启动的完整流程:

image.png
3.1 设置运行环境
image.png
3.2 加载共享缓存
image.png
3.3 实例化主程序
image.png
3.4 加载插入的动态库
image.png
3.5 链接主程序
image.png
3.6 链接插入的动态库
image.png
3.7 执行弱符号绑定
image.png
3.8 执行初始化方法
image.png image.png image.png image.png image.png

3.9 寻找main入口
image.png

以上就是完整的app启动流程。


这里对3.8 执行初始化方法 最后一步的2个内容进行继续探究:

1. notifySingle如何告知外部

image.png image.png image.png

回调函数通过函数指针调用的函数
函数指针(地址)作为参数传递给另一个函数,当该指针用来调用所指向的函数时,我们就说这是回调函数
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应

我们探索一下load_images函数内部:

load_images函数

image.png image.png image.png
    1. 明确+load方法的加载时机;
    1. 明确只有+load这个名称才有效(因为sel已固定,系统只检查load这个方法名)

对比在+load函数断点处打印的堆栈信息,与我们源码分析过程完全吻合

image.png

notifySingledyld跨库到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初始化

image.png image.png

发现有doImageInitdoModInitFunctions2个初始化操作

image.png image.png

在测试代码的c++构造函数constructor处加入断点bt打印堆栈信息检验,确实是在doModInitFunctions函数内完成了实现。

image.png

探索_objc_init调用时机

objc4源码中搜索_objc_init,加入断点,运行测试代码。

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++ 的执行顺序?是否已非常清晰。

强烈建议阅读以下官方资源:

  1. WWDC 2016 Optimizing App Startup Time
  • 快速熟悉Mach-O结构(后续有变动)
  • dyld如何将mach-o信息映射到内存中
  • app启动流程(旧版)和优化建议
  1. WWDC 2017 App Startup Time: Past, Present, and Future
  • 介绍dyld历史,引出dyld3(围绕性能、安全、占用资源进行优化)
  1. WWDC 2019 Optimizing App Launch
  • 介绍App Launch工具,优化启动时间

本文仅简单记录dyld的大致启动流程,部分细节并未展开拓展。源码的探索之旅继续进行...

上一篇 下一篇

猜你喜欢

热点阅读