OC底层-dyld应用加载流程(上)

819 阅读5分钟

之前文章分析了消息转发,这篇文章探索下我们的APP启动后做了什么,APP启动是直接进入main函数吗?我们常说的在main函数之前启动runloop是发生在什么时候呢?今天就先去悄悄瞄一眼dyld

准备工作

前戏

我们在探索前,先来看下怎么找到程序的加载流程呢?我想很多人都会想到断点调式,但是我们应该断到哪里呢?是在main函数吗?那么我们先试下:

image.png

我们能够看到是无法看到加载流程,只知道main函数之前调用了一个start函数,具体就看不到了,然后我们都知道load方法的加载就是在main函数之前,那么我们能否在load函数中添加断点看下呢?按照猜想,我们创建一个空项目,然后在main函数中添加断点,然后在viewController.m中实现load函数,同时添加断点。然后我们运行项目看下效果:

image.png

我们可以看到load方法的断点确实比main函数的断点先走的,然后同时看下左侧的堆栈信息,首先是从_dyld_start开始,然后到load_images之后就开始调用了load方法,那么我们就去看下这个过程发生了什么。

dyld源码探索-_dyld_start

1、在全局搜索_dyld_start,我们能够找到如下的代码:

image.png

2、因为我们需要看_dyld_start的实现,所以我们直接到汇编代码中看下具体实现,发现里面代码比较多,都是根据不同的架构区分,而且也相差不大,基本上逻辑差不多,所以我们直接找到比较熟悉的__i386__的,看下具体代码:

image.png

3、这里重点是我们通过分析发现一个# call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)注释,每个架构下都是有这个,所以我们直接看下这个函数调用的实现,从函数格式我们就能看出这个是一个C++函数,所以我们直接搜索dyldbootstrap命名空间,然后找到start函数的实现:

image.png

4、我们可以看到这块主要做了一些对于macho_header的处理,然后去调用了dyldmain函数,这块的main函数不是我们程序中main.m中的main函数。

  • 补充知识点

    1、macho_header:是Mach-o可执行文件的一部分,也就是它的头,我们可以通过MachOView打开一个运行后的APP的Mach-o,能够看到以下图类似的内容:

    Mach-o

    2、Mach-o一般包含一下三个部分:

    • Header:里面保存Mach-O的一些基本信息,包括平台、文件类型、指令数、指令总大小,dyld标记Flags等等。

    • Load Commands:紧跟Header,加载Mach-O文件时会使用这部分数据确定内存分布,对系统内核加载器和动态连接器起指导作用。

    • Data:每个segment的具体数据保存在这里,包含具体的代码、数据等等。

dyld源码探索-_main

1、检测运行环境、准备运行环境等等工作:

image.png

2、dyld开始加载

image.png

3、共享缓存

image.png

4、初始化主程序

image.png

5、加载库

image.png

6、链接库

image.png

7、运行所有的初始化方法

image.png

8、找到主程序的main函数

image.png

9、通过上面得到的主程序main函数的入口。

sMainExecutable

1、我们在看dyld加载流程的时候,看到sMainExecutable是通过instantiateFromLoadedImage获取,那么我们看下这个函数的实现:

image.png

2、在上面函数中通过instantiateMainExecutable初始化获取到了一个ImageLoader类型的对象,那么我们看下这个函数的具体实现:

image.png

3、我们可以通过苹果的注释看到,这个函数就是为了创建一个主程序的镜像image,然后获取到Mach-o文件的segCountlibCount,方便在dyldmain函数中进行校验,同时也基于加载命令内容实例化具体类。

initializeMainExecutable

1、这个函数是运行所有初始化,也就是主程序执行流程了,那么我们看下这个函数的具体实现:

image.png

2、这个函数中主要是为任何插入的dylibs运行initialzers,然后我们继续进入runInitializers函数看下具体实现:

image.png

3、在这个函数中主要是调用了processInitializers函数实现向上依赖的初始化,我们看下具体代码:

image.png

4、在这个函数中对images列表中的所有映像调用递归init,构建一个未初始化向上依赖项的新列表,然后我们看下recursiveInitialization的具体实现:

image.png

5、在这个函数中我们能够看到先进行判断是否是向上依赖,然后通知objc将要开始初始化,然后进行初始化操作,最后通知所有监听者初始化完成。

6、下面我们重点去看下准备初始化、初始化、通知初始化完成这三个过程。

notifySingle

1、在上面过程我们发现,无论是准备初始化还是初始化完成的通知都是通过一个notifySingle来完成的,那么我们先看下这个函数的实现:

image.png

2、在上面这个函数中,主要是看sNotifyObjCInit,然后我们看下这个函数的实现:

image.png

3、发现这个函数并没有实现,但是在registerObjCNotifiers函数中进行了初始化,那么我们看下这个registerObjCNotifiers函数在哪调用的:

image.png

4、特别简单,然后我们再看下这个函数_dyld_objc_notify_register函数在哪里调用的呢?经过搜索并没有找到,于是乎看方法的注释信息,找到了一个比较有用的,如下:

image.png

5、注释中明确写到,仅供objc运行时使用,所以立马想到了objc源码,于是在objc源码中进行搜索,找到了如下代码:

image.png

6、那么我们就可以得到如下一个结论:

// 记录要调用的函数
sNotifyObjCMapped = mapped = &map_images;
sNotifyObjCInit = init = load_images;
sNotifyObjCUnmapped = unmapped = unmap_image;

7、到这里我们就明白了,objc_init调起dyld_dyld_objc_notify_register函数,进行镜像文件的加载以及初始化操作。

总结

本篇文章我们大概瞄了一眼dyld加载镜像文件的一些过程,我们知道是通过objc_init调用的,那么这个是什么时候调用的呢?请听下回分析。

611626865520_.pic_hd.jpg