OC底层原理之-App启动过程(dyld加载流程)

4,292 阅读12分钟

前言

我将之前写的文章归纳到了OC底层原理系列通过OC底层原理系列的对象篇,我们知道了OC对象有个比较全面的认识,通过类篇,我们对OC的类也有清晰的认识,通过消息发送篇,对消息的查找,转发有了深入的了解。应用程序内容也就是对象,类以及方法调用(消息发送)这些内容。所以这篇文章我们就要讲解App应用程序的加载过程

探究App启动流程

探究App启动流程,我们最先想到的是通过代码来验证

通过代码验证App启动顺序

准备如下代码

@implementation ViewController
+ (void)load{
    NSLog(@"%s",__func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
}
@end

__attribute__((constructor)) void kcFunc(){
    printf("来了 : %s \n",__func__);
}
int main(int argc, char * argv[]) {
    NSLog(@"进入main函数");
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

我们在ViewController中些了load方法,在Main.m中写了一个C++的静态方法,在main函数中加了日志,我们运行起来,看下打印的先后顺序,打印结果如下: 那么问题来了,我们都知道main函数式App的唯一入口,但是load方法却是最先走的,而静态方法是后走,main函数反而是最后执行的。这是为什么?

补充动静态库以及可执行文件

我们知道在App运行过程中会依赖底层很多的库,这些库有静态库和动态库

  • 静态库通常以.a,.lib或者.framework结尾,动态库以.tbd,.so,.framework结尾
  • 静态库:链接时,静态库会被完整的复制到可执行文件中,被多次使用就会有多份冗余拷贝
  • 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序公用,节省内存

补充:这里的静态,动态库主要是苹果官方的,我们自己的私有动态库是不会公用的,谢谢科技农民工指出来不严谨的地方 我们知道.h,.m的类在程序运行时先进行预编译,之后进行编译,编译完成后会进行汇编,在汇编结束后会进入一个阶段叫连接(把所有的代码链接到我们的程序中),最后会生成一个可执行文件。我们上面的代码就有可执行文件(下图红框) 上面我们将了App运行需要加载依赖库,需要加载.h,.m文件,那么谁来决定加载这些东西的先后顺序呢?这就是我们今天要说的主角dyld(连接器)。就是由它来决定加载内容的先后顺序。

app:images(镜像文件)->dyld:读到内存(也就是加表里),启动主程序 - 进行link - 一些必要对象的初始化(runtime,libsysteminit,OS_init的初始化)。

下面我们就来学习dyld。

探究dyld

分析主程序初始化过程

首先我们去官网上去下载dyld源码,打开源码,我们看到里面有很多东西 那么问题来了,我们应该从哪开始看起。我们在load方法加断点,运行项目,运行到断点后,我们打印bt(这是思路)。 我们看到最开始dyld是从_dyld_start开始的。然后我们全局搜索_dyld_start,找到我们要找的方法,下面我们看的是x86(不同的架构,还有arm,i386)。 我们发现上面都是汇编,基本上看不懂,那怎么办?这种情况我们可以通过注释去辅助理解,因为这里面的注释还是蛮详细的。我们看代码 发现这个_dyld_start会调用dyldbootstrap,下面我们去找下dyldbootstrap方法看看 我们找到这个方法,发现内容很多,下面我们就捡主要的进行说明。我们往下浏览方法,发现start方法 很多内容我们不需要了解,我们只需要去关注重要方法。

macho_header:macho的头文件。我们将可执行文件放到烂苹果中读到的目录信息就是macho文件。

我们点击_main函数进入下面方法 这个方法大概有700多行代码,我们还是捡重要的方法就理解。因为这个方法返回的是result,那么我们就需要找result在那里进行赋值操。我们全局搜result 我们发现都会调用sMainExecutable,也就是都会用sMainExecutable进行赋值,下面我们就搜索sMainExecutable发现下面代码,这个代码就是对sMainExecutable进行初始化 我们点instantiateFromLoadedImage进去(截图我会把注释也截进去,可以看看注释) 如何初始化,就是在家我们的ImageLoader,我们再看下方法instantiateMainExecutable 这个方法里面重要方法是sniffLoadCommands,我们搜一下sniffLoadCommands,这里有个窍门,因为sniffLoadCommands第一个参数是mh,mh的前缀是const,所以我们可以这样搜 点击需要看的方法 我们看到一些内容跟烂苹果里Load Commands下有相同的(只截取部分,相同不少) 其实这个方法就是在组建整个主程序的格式。再回到下图 这其实就是创建ImageLoader,加载到Image里面,并返回。所以sMainExecutable就是创建后的主程序。

分析_main函数过程

上面我们大致分析了下主程序的初始化过程,下面我们再看下这个_main方法都做了什么

判断版本信息 当前平台 当前的MO地址 设置crash以及log地址,设置上下文信息 设置环境变量,envp就是_main函数的参数,它是所有环境变量的数组,就是将环境变量插入进去 对共享缓存进行处理 主程序的初始化(上面讲过了) 插入动态库 link主程序 link我们所有的image(通过上面两个可以知道,必须先link主程序,然后在link所有的image 绑定弱符号 执行所有的初始化方法 查找main函数入口

下面我们总结下整个过程:

分析initializeMainExecutable初始化过程

上面我们讲到_main函数会执行所有的initializers 下面我们就看看这个方法实现过程,点击进入下面方法 全局搜索runInitializers(initializeMainExecutable方法中,runInitializers通过字面意思就知道这个是初始化方法,故搜索它) 我们看到runInitializers方法,每行分析一遍,发现processInitializers才是分析的重点。我们所搜processInitializers方法 我们根据经验可知,红框内的方法是重要方法。这个方法是递归调用(之所以是递归,是因为我们的动态库或者静态库会引入其它类库,而且表是个多表结构,所以需要递归实例化)。我们搜索recursiveInitialization方法 我们发现这个方法代码不少,哪些使我们需要研究的重要方法呢?(recursiveSpinLock(lock_info);这个是递归锁) 这个方法就是循环判断是否都加载过,没有就执行dependentImage,因为我们前面说的动态库可能会依赖其它库。看下图 我们在弄源码的时候,KCObjc运行时依赖objc(objc)的,就是这个意思。上面的方法是扩展但不是重点方法。 红框才是重点方法,但是1595行代码跟1603行代码都调用了notifySingle,我们也去看下notifySingle代码 代码也很多,我们要学会找重要的方法去研究 红框方法是重点,我们看下sNotifyObjCInit,全局搜一下 我们发现sNotifyObjCInit就等于init方法,好像没有找到实现方法,那后面应该怎么办?我们看到sNotifyObjCInit是在registerObjCNotifiers方法中赋值,那么调用registerObjCNotifiers方法是不是就会给sNotifyObjCInit值,所以我们需要看registerObjCNotifiers就有可能能找到sNotifyObjCInit的实现。这个是思路,我们搜索registerObjCNotifiers 我们发现在_dyld_objc_notify_register里调用了registerObjCNotifiers方法,_dyld_objc_notify_register这个方法在我们的Objc源码中存在,我们去源码搜一下 是在_objc_init的方法中调用 总结下:_dyld_objc_notify_register相当于一个回调函数,也就是该方法的调用是在_objc_init调用的方法里调用(这是我们在Objc源码搜到的结果)类推就是调用初始化方法才回去调用_dyld_objc_notify_register 回到开始的方法,下图 我们来研究红框的方法,这个方法才是初始化的方法,我们点进去,发现ImageLoaderMachO方法中有doImageInit方法,我们点击doImageInit进去 方法还是很多,我们看下第2258行代码,意思就是通过内存地址平移得到要的方法,我们举个例子 我们看到上图的main函数内存地址,我们将这个地址不断的平移,得到其他方法。2258行代码意思就是不断平移得到初始化方法进行调用 这个方法就是libSystem必须先初始化,否则就会报错 总结:上面我们研究了初始化的过程,最后是由内存地址不断平移拿到初始化方法进行调用下面我们来验证下

验证initializeMainExecutable流程

我们在Objc源码的_objc_init打上断点 运行代码,来到断点处,我们看下堆栈信息 我们看到libSystem_initializer代码,去下载源码:Libsystem,我们下载的源码版本是Libsystem-1252.250.1。我们打开源码,搜索libSystem_initializer,我们找到代码 代码很多,只截取了部分,我们对方法向下看

149行对内核的初始化,151行对平台相关初始化,153行是对线程的初始化(初始化后我们的GCD才能使用),155行是对libc的初始化,158行是对malloc初始化。

168行是对dyld进行初始化(dyld_start是dyld并没有初始化,dyld也是一个库),170行是对libdispatch进行初始化。我们上面的堆栈信息里也看到了libdispatch的初始化调用

我们看到调用libdispatch_init是在libdispatch.dylib中,我们去下载libdispatch.dylib源码,下载地址:libdispatch-1173.40.5 我们在libdispatch源码里搜索libdispatch_init(代码很多截取部分) 我们往下看,会有_dispatch_thread一些creat操作(GCD相关)。 看到最后我们发现_os_object_init的初始化,我们搜一下 我们发现这个第75行代码就是_objc_init(),也就是从_os_object_init跳入到_objc_init,进入runtime的初始化,上面我们讲了_objc_init会调用_dyld_objc_notify_register,然后就对sNotifyObjCInit赋值。 再回到下图的方法 我们在doInitialization方法调用之后在进行notifySingle,而notifySingle就会跳到sNotifyObjCInit,sNotifyObjCInit()才会执行

总结下流程

我们用一张流程图来看initializeMainExecutable的整个过程

解释App启动流程

文章开始说的在App启动时我们发现现在load方法,再走C++静态方法,接着走main函数,下面我们来解释下为什么。

验证load先调用

我们后面分析了先走初始化方法,在Objc源码中应该先走_objc_init方法,它最后会调用_dyld_objc_notify_register,传入load_images,下面我们看下load_images都有什么 我们看到最后是调用了call_load_methods,我们去看下call_load_methods里面包含什么 我们在350行-360行是进行的do-while循环,再循环里面第353行执行call_class_loads,我们感觉答案近了,我们看下call_class_loads里面的代码 196行-205行执行的for循环,在204行我们看到会执行load方法。所以在调用_dyld_objc_notify_register之前,我们就已经将load方法调用了。下面我们看下C++静态方法在哪调用呢?

验证C++静态方法调用

我们上面分析ImageLoaderMachO只分析了doImageInit,而为分析doModInitFunctions,我们看下图 我们点击ImageLoaderMachO看下方法实现 方法内容很多,只截取了部分,这个方法就是调用应用内部所有的C++静态方法。下面我们验证下,我们在之前写的C++静态方法那打断点,运行代码,打印bt(打印堆栈信息)。我们看下图 我们看到#1那一行就是doModInitFunctions,说明C++的静态方法就是在执行doModInitFunctions下执行。

验证main函数

这时候我们转成汇编看下 我们下一步等kcfun方法走完,我们直接下一步,就会来到main函数中 main函数是被编译到内中的,而且固定写死的,编译器找到main函数会加载到内存中,验证:如果我们更改main函数名称 我们吧main改成ll_main,再去运行 我们看到报错了,告诉我们找不到main函数,这部分其实在dyld源码中也有所体现,下面我们搜下_dyld_start看下,我们分别看下x86,i386,arm64 上面看到man函数在下层就已经写死了。

最后

这边文章我们主要讲了程序运行的过程,主要介绍了dyld的流程,后面我们又详细讲了initializeMainExecutable的过程,也解释了方法调用顺序为什么是load在前而main函数在后。东西比较多,需要慢慢去理解。有什么错误的地方还希望各位指正出来!文章说的最多的就是找重要的方法,在后面的探究中,一定要知道哪些方法是重要方法。首先看看方法有无返回值,有看返回值在哪赋值(过程中要看注释,来确保正确)。如果无返回值,我们就要明白自己研究的是什么,这个方法用来做什么,然后去找相关代码,看注释,必要时看看后面的方法来确保正确性