之前文章分析了消息转发,这篇文章探索下我们的APP启动后做了什么,APP启动是直接进入main
函数吗?我们常说的在main
函数之前启动runloop
是发生在什么时候呢?今天就先去悄悄瞄一眼dyld
。
准备工作
前戏
我们在探索前,先来看下怎么找到程序的加载流程呢?我想很多人都会想到断点调式
,但是我们应该断到哪里呢?是在main
函数吗?那么我们先试下:
我们能够看到是无法看到加载流程,只知道main
函数之前调用了一个start
函数,具体就看不到了,然后我们都知道load
方法的加载就是在main
函数之前,那么我们能否在load
函数中添加断点看下呢?按照猜想,我们创建一个空项目,然后在main
函数中添加断点,然后在viewController.m
中实现load
函数,同时添加断点。然后我们运行项目看下效果:
我们可以看到load
方法的断点确实比main
函数的断点先走的,然后同时看下左侧的堆栈信息,首先是从_dyld_start
开始,然后到load_images
之后就开始调用了load
方法,那么我们就去看下这个过程发生了什么。
dyld源码探索-_dyld_start
1、在全局搜索_dyld_start
,我们能够找到如下的代码:
2、因为我们需要看_dyld_start
的实现,所以我们直接到汇编代码中看下具体实现,发现里面代码比较多,都是根据不同的架构区分,而且也相差不大,基本上逻辑差不多,所以我们直接找到比较熟悉的__i386__
的,看下具体代码:
3、这里重点是我们通过分析发现一个# call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
注释,每个架构下都是有这个,所以我们直接看下这个函数调用的实现,从函数格式我们就能看出这个是一个C++
函数,所以我们直接搜索dyldbootstrap
命名空间,然后找到start
函数的实现:
4、我们可以看到这块主要做了一些对于macho_header
的处理,然后去调用了dyld
的main
函数,这块的main
函数不是我们程序中main.m
中的main
函数。
-
补充知识点
1、
macho_header
:是Mach-o
可执行文件的一部分,也就是它的头,我们可以通过MachOView
打开一个运行后的APP的Mach-o
,能够看到以下图类似的内容:2、
Mach-o
一般包含一下三个部分:-
Header
:里面保存Mach-O
的一些基本信息,包括平台、文件类型、指令数、指令总大小,dyld
标记Flags
等等。 -
Load Commands
:紧跟Header
,加载Mach-O文件
时会使用这部分数据确定内存分布,对系统内核加载器和动态连接器起指导作用。 -
Data
:每个segment
的具体数据保存在这里,包含具体的代码、数据等等。
-
dyld源码探索-_main
1、检测运行环境、准备运行环境等等工作:
2、dyld
开始加载
3、共享缓存
4、初始化主程序
5、加载库
6、链接库
7、运行所有的初始化方法
8、找到主程序的main
函数
9、通过上面得到的主程序main
函数的入口。
sMainExecutable
1、我们在看dyld
加载流程的时候,看到sMainExecutable
是通过instantiateFromLoadedImage
获取,那么我们看下这个函数的实现:
2、在上面函数中通过instantiateMainExecutable
初始化获取到了一个ImageLoader
类型的对象,那么我们看下这个函数的具体实现:
3、我们可以通过苹果的注释看到,这个函数就是为了创建一个主程序的镜像image
,然后获取到Mach-o
文件的segCount
和libCount
,方便在dyld
的main
函数中进行校验,同时也基于加载命令内容实例化具体类。
initializeMainExecutable
1、这个函数是运行所有初始化,也就是主程序执行流程了,那么我们看下这个函数的具体实现:
2、这个函数中主要是为任何插入的dylibs
运行initialzers
,然后我们继续进入runInitializers
函数看下具体实现:
3、在这个函数中主要是调用了processInitializers
函数实现向上依赖的初始化,我们看下具体代码:
4、在这个函数中对images
列表中的所有映像调用递归init
,构建一个未初始化向上依赖项的新列表,然后我们看下recursiveInitialization
的具体实现:
5、在这个函数中我们能够看到先进行判断是否是向上依赖,然后通知objc
将要开始初始化,然后进行初始化操作,最后通知所有监听者初始化完成。
6、下面我们重点去看下准备初始化、初始化、通知初始化完成这三个过程。
notifySingle
1、在上面过程我们发现,无论是准备初始化还是初始化完成的通知都是通过一个notifySingle
来完成的,那么我们先看下这个函数的实现:
2、在上面这个函数中,主要是看sNotifyObjCInit
,然后我们看下这个函数的实现:
3、发现这个函数并没有实现,但是在registerObjCNotifiers
函数中进行了初始化,那么我们看下这个registerObjCNotifiers
函数在哪调用的:
4、特别简单,然后我们再看下这个函数_dyld_objc_notify_register
函数在哪里调用的呢?经过搜索并没有找到,于是乎看方法的注释信息,找到了一个比较有用的,如下:
5、注释中明确写到,仅供objc
运行时使用,所以立马想到了objc
源码,于是在objc
源码中进行搜索,找到了如下代码:
6、那么我们就可以得到如下一个结论:
// 记录要调用的函数
sNotifyObjCMapped = mapped = &map_images;
sNotifyObjCInit = init = load_images;
sNotifyObjCUnmapped = unmapped = unmap_image;
7、到这里我们就明白了,objc_init
调起dyld
的_dyld_objc_notify_register
函数,进行镜像文件的加载以及初始化操作。
总结
本篇文章我们大概瞄了一眼dyld
加载镜像文件的一些过程,我们知道是通过objc_init
调用的,那么这个是什么时候调用的呢?请听下回分析。