iOS 底层探索系列
- iOS 底层探索 - alloc & init
- iOS 底层探索 - calloc 和 isa
- iOS 底层探索 - 类
- iOS 底层探索 - cache_t
- iOS 底层探索 - 方法
- iOS 底层探索 - 消息查找
- iOS 底层探索 - 消息转发
- iOS 底层探索 - 应用加载
- iOS 底层探索 - 类的加载
- iOS 底层探索 - 分类的加载
- iOS 底层探索 - 类拓展和关联对象
- iOS 底层探索 - KVC
- iOS 底层探索 - KVO
iOS 查漏补缺系列
App 从被用户在主屏幕上点击之后就开启了它的生命周期,那么在这之中,究竟发生了什么呢?让我们从 App 启动开始探索。在探索之前,我们需要熟悉一些前导知识点。
一、前导知识
以下参考自 WWDC 2016 Optimizing App Startup Time :
1.1 Mach-O
Mach-O is a bunch of file types for different run time executables.
Mach-O是iOS系统不同运行时期可执行的文件的文件类型统称。
维基百科上关于 Mach-O 的描述:
Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品,Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。 大多数基于 Mach 内核的操作系统都使用 Mach-O。NeXTSTEP、OS X 和 iOS 是使用这种格式作为本地可执行文件、库和对象代码的例子。
Mach-O 有三种文件类型: Executable、Dylib、Bundle
Executable类型
So the first executable, that's the main binary in an app, it's also the main binary in an app extension.
executable是app的二进制主文件,同时也是app extension的二进制主文件
我们一般可以在 Xcode 项目中的 Products 文件夹中找到它:
如上图箭头所示,App加载流程 就是我们 App 的二进制主文件。
Dylib类型
A dylib is a dynamic library, on other platforms meet, you may know those as DSOs or DLLs.
dylib是动态库,在其他平台也叫DSO或者DLL。
对于接触 iOS 开发比较早的同学,可能知道我们在 Xcode 7 之前添加一些比如 sqlite 的库的时候,其后缀名为 dylib,而 Xcode 7 之后后缀名都改成了 tbd。
这里引用 StackoverFlow 上的一篇回答。
So it appears that the .dylib file is the actual library of binary code that your project is using and is located in the /usr/lib/ directory on the user's device. The .tbd file, on the other hand, is just a text file that is included in your project and serves as a link to the required .dylib binary. Since this text file is much smaller than the binary library, it makes the SDK's download size smaller. 看起来
.dylib文件是项目中真正使用到的二进制库文件,它位于用户设备上的/usr/lib目录下。而.tbd文件,只是位于你项目中的一个文本文件,它扮演的是链接到真正的.dylib二进制文件的角色。因为文本文件的大小远远小于二进制文件的大小,所以让Xcode 的SDK` 的下载大小更小。
这里再插一句,那么有动态库,肯定就有静态库,它们的区别是什么呢?
我们先梳理一下整个的编译过程。
当然,这个过程中间其实还设计到编译器前端的 词法分析、语法分析、语义分析、优化 等流程,我们在后面探索 LLVM 和 Clang 的时候会详细介绍。
回到刚才的话题,静态库和动态库的区别:
Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime.
静态库和动态库都是编译好的二进制文件,只是用法不同。那为什么要分动态和静态库呢?
通过上面两幅图我们可以知道:
- 静态库表现为:在链接阶段会将汇编生成的目标文件与引用的库一起链接打包进可执行文件中。
- 动态库表现为:程序编译并不会链接到目标代码中,在程序可执行文件里面会保留对动态库的引用。其中,动态库分为动态链接库和动态加载库。
- 动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。在
Linked Framework and Libraries设置的一些share libraries。【随着程序启动而启动】 - 动态加载库:当需要的时候再使用
dlopen等通过代码或者命令的方式来加载。【在程序启动之后】
- 动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。在
Bundle类型
Now a bundle's a special kind of dylib that you cannot link against, all you can do is load it at run time by an dlopen and that's used on a Mac OS for plug-ins. 现阶段
Bundle是一种特殊类型的dylib,你是无法对其进行链接的。你所能做的是在Runtime运行时去通过dlopen来加载它,它可以在macOS上用于插件。
Image和Framework
Image refers to any of these three types. 镜像文件包含了上述的三种文件类型
a framework is a dylib with a special directory structure around it to holds files needed by that dylib. 有很多东西都叫做
Framework,但在本文中,Framework指的是一个dylib,它周围有一个特殊的目录结构来保存该dylib所需的文件。
1.1.1 Mach-O 结构分析
1.1.1.1 segment 段
Mach-O 镜像文件是由 segments 段组成的。
- 段的名称为大写格式
所有的段都是page size的倍数。 - arm64 上段大小为
16KB - 其它架构为
4KB
这里再普及一下虚拟内存和内存页的知识:
具有
VM机制的操作系统,会对每个运行的进程创建一个逻辑地址空间logical address space或者叫虚拟地址空间virtual address space;该空间的大小由操作系统位数决定:32位的操作系统,其逻辑地址空间的大小为4GB,64位的操作系统为18 exabyes(其计算方式是2^32||2^64)。
虚拟地址空间(或者逻辑地址空间)会被分为相同大小的块,这些块被称为内存页(page)。计算机处理器和它的内存管理单元(MMU - memory management uinit)维护着一张将程序的逻辑地址空间映射到物理地址上的分页表
page table。
在
masOS和早版本的iOS中,分页的大小为4kB。在之后的基于A7和A8的系统中,虚拟内存(64位的地址空间)地址空间的分页大小变为了16KB,而物理RAM上的内存分页大小仍然维持在4KB;基于A9及之后的系统,虚拟内存和物理内存的分页都是16KB。
1.1.1.2 section
在 segment 段内部还有许多的 section 区。section 名称为小写格式。
But sections are really just a subrange of a segment, they don't have any of the constraints of being page size, but they are non-overlapping. 但是
sections节实际上只是一个segment段的子范围,它们没有页面大小的任何限制,但是它们是不重叠的。
通过 MachOView 工具查看 app 的二进制可执行文件可以查看到:
1.1.1.3 常见的 segments
__TEXT:代码段,包括头文件、代码和常量。只读不可修改
__DATA:数据段,包括全局变量, 静态变量等。可读可写。
__LINKEDIT:如何加载程序, 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。
1.1.2 Mach-O Universal Files
Mach-O 通用文件,将多种架构的 Mach-O 文件合并而成。它通过 header 来记录不同架构在文件中的偏移量,segement 占多个分页,header占一页的空间。可能有人会觉得 header 单独占一页会浪费空间,但这有利于虚拟内存的实现。
1.2 虚拟内存
虚拟内存是一层间接寻址。
虚拟内存解决的是管理所有进程使用物理 RAM 的问题。通过添加间接层来让每个进程使用逻辑地址空间,它可以映射到 RAM 上的某个物理页上。这种映射不是一对一的,逻辑地址可能映射不到 RAM 上,也可能有多个逻辑地址映射到同一个物理 RAM 上。
- 针对第一种情况,当进程要存储逻辑地址内容时会触发
page fault。 - 而第二种情况就是多进程共享内存。
- 对于文件可以不用一次性读入整个文件,可以使用分页映射
mmap()的方式读取。也就是把文件某个片段映射到进程逻辑内存的某个页上。当某个想要读取的页没有在内存中,就会触发page fault,内核只会读入那一页,实现文件的懒加载。也就是说Mach-O文件中的__TEXT段可以映射到多个进程,并可以懒加载,且进程之间共享内存。 __DATA段是可读写的。这里使用到了Copy-On-Write技术,简称COW。也就是多个进程共享一页内存空间时,一旦有进程要做写操作,它会先将这页内存内容复制一份出来,然后重新映射逻辑地址到新的RAM页上。也就是这个进程自己拥有了那页内存的拷贝。这就涉及到了clean/dirty page的概念。dirty page含有进程自己的信息,而clean page可以被内核重新生成(重新读磁盘)。所以dirty page的代价大于clean page。
1.3 多进程加载 Mach-O 镜像
- 所以在多个进程加载
Mach-O镜像时__TEXT和__LINKEDIT因为只读,都是可以共享内存的,读取速度就会很快。 - 而
__DATA因为可读写,就有可能会产生dirty page,如果检测到有clean page就可以直接使用,反之就需要重新读取DATA page。一旦产生了dirty page,当dyld执行结束后,__LINKEDIT需要通知内核当前页面不再需要了,当别人需要的使用时候就可以重新clean这些页面。
1.4 ASLR
ASLR (Address Space Layout Randomization) 地址空间布局随机化,镜像会在随机的地址上加载。
1.5 Code Signing
可能我们认为 Xcode 会把整个文件都做加密 hash 并用做数字签名。其实为了在运行时验证 Mach-O 文件的签名,并不是每次重复读入整个文件,而是把每页内容都生成一个单独的加密散列值,并存储在 __LINKEDIT 中。这使得文件每页的内容都能及时被校验确并保不被篡改。
1.6 exec()
Exec is a system call. When you trap into the kernel, you basically say I want to replace this process with this new program.
exec()是一个系统调用。系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(因为使用ASLR)。并将起始位置到0x000000这段范围的进程权限都标记为不可读写不可执行。如果是32位进程,这个范围至少是4KB;对于64位进程则至少是4GB。NULL指针引用和指针截断误差都是会被它捕获。这个范围也叫做PAGEZERO。
1.7 dyld
Unix 的前二十年很安逸,因为那时还没有发明动态链接库。有了动态链接库后,一个用于加载链接库的帮助程序被创建。在苹果的平台里是
dyld,其他Unix系统也有ld.so。 当内核完成映射进程的工作后会将名字为dyld的Mach-O文件映射到进程中的随机地址,它将PC寄存器设为dyld的地址并运行。dyld在应用进程中运行的工作是加载应用依赖的所有动态链接库,准备好运行所需的一切,它拥有的权限跟应用一样。
1.8 dyld 流程
- Load dylibs
从主执行文件的
header获取到需要加载的所依赖动态库列表,而header早就被内核映射过。然后它需要找到每个dylib,然后打开文件读取文件起始位置,确保它是Mach-O文件。接着会找到代码签名并将其注册到内核。然后在dylib文件的每个segment上调用mmap()。应用所依赖的dylib文件可能会再依赖其他dylib,所以dyld所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载100到400个dylib文件,但大部分都是系统dylib,它们会被预先计算和缓存起来,加载速度很快。
- Fix-ups
在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是
Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个dylib的调用另一个dylib。这时需要加很多间接层。 现代code-gen被叫做动态 PIC(Position Independent Code),意味着代码可以被加载到间接的地址上。当调用发生时,code-gen实际上会在__DATA段中创建一个指向被调用者的指针,然后加载指针并跳转过去。所以dyld做的事情就是修正(fix-up)指针和数据。Fix-up有两种类型,rebasing和binding。
- Rebasing 和 Binding
Rebasing:在镜像内部调整指针的指向 Binding:将指针指向镜像外部的内容
dyld 的时间线由上图可知为:
Load dylibs -> Rebase -> Bind -> ObjC -> Initializers
1.9 dyld2 && dyld3
在 iOS 13 之前,所有的第三方 App 都是通过 dyld 2 来启动 App 的,主要过程如下:
- 解析
Mach-O的Header和Load Commands,找到其依赖的库,并递归找到所有依赖的库 - 加载
Mach-O文件 - 进行符号查找
- 绑定和变基
- 运行初始化程序
dyld3 被分为了三个组件:
- 一个进程外的
MachO解析器- 预先处理了所有可能影响启动速度的
search path、@rpaths和环境变量 - 然后分析
Mach-O的Header和依赖,并完成了所有符号查找的工作 - 最后将这些结果创建成了一个启动闭包
- 这是一个普通的
daemon进程,可以使用通常的测试架构
- 预先处理了所有可能影响启动速度的
- 一个进程内的引擎,用来运行启动闭包
- 这部分在进程中处理
- 验证启动闭包的安全性,然后映射到
dylib之中,再跳转到main函数 - 不需要解析
Mach-O的Header和依赖,也不需要符号查找。
- 一个启动闭包缓存服务
- 系统
App的启动闭包被构建在一个Shared Cache中, 我们甚至不需要打开一个单独的文件 - 对于第三方的
App,我们会在App安装或者升级的时候构建这个启动闭包。 - 在
iOS、tvOS、watchOS中,这这一切都是App启动之前完成的。在macOS上,由于有Side Load App,进程内引擎会在首次启动的时候启动一个daemon进程,之后就可以使用启动闭包启动了。
- 系统
dyld 3 把很多耗时的查找、计算和 I/O 的事前都预先处理好了,这使得启动速度有了很大的提升。
好了,先导知识就总结到这里,接下来让我们调整呼吸进入下一章~
二、App 加载分析
我们在探索 iOS 底层的时候,对于对象、类、方法有了一定的认知哦,接下来我们就一起来探索一下应用是怎么加载的。
我们直接新建一个 Single View App 的项目,然后在 main.m 中打一个断点:
然后我们可以看到在 main 方法执行前有一步 start,而这一流程是由 libdyld.dylib 这个动态库来执行的。
这个现象说明了什么呢?说明我们的 app 在 main 函数执行之前其实还通过 dyld 做了很多事情。那为了搞清楚具体的流程,我们不妨从 Apple OpenSource 上下载 dyld 的源码来进行探索。
我们选择最新的 655.1.1 版本:
三、dyld 源码分析
面对 dyld 的源码,我们不可能一行一行的去分析。我们不妨在刚才创建的项目中断点一下 load 方法,看下调用堆栈:
这一次我们发现,load 方法的调用要早于 main 函数的调用,其次,我们得到了一个非常有价值的线索: _dyld_start。
3.1 _dyld_start
我们直接在 dyld 655.1.1 中全局搜索这个 _dyld_start,我们可以来到 dyldStartup.s 这个汇编文件,然后我们聚焦于 arm64 架构下的汇编代码:
对于这里的汇编代码,我们肯定也没必要逐行分析,我们直接定位到 bl 语句后面(bl 在汇编层面是跳转的意思):
bl __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
我们可以看到这里有一行注释:
// call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
这行注释的意思是调用位于 dyldbootstrap 命名空间下的 start 方法,我们继续搜索一下这个 start 方法,结果位于 dyldInitialization.cpp 文件(从文件名我们可以看出该文件主要是用来初始化 dyld),这里查找 start 的时候可能会有很多结果,我们其实可以先搜索命名空间,再搜索 start 方法。
3.2 dyldbootstrap::start
start 方法源码如下:
//
// This is code to bootstrap dyld. This work in normally done for a program by dyld and crt.
// In dyld we have to do this manually.
//
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[],
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
slide = slideOfMainExecutable(dyldsMachHeader);
bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
shouldRebase = true;
#endif
if ( shouldRebase ) {
rebaseDyld(dyldsMachHeader, slide);
}
// allow dyld to use mach messaging
mach_init();
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
我们刚才探索到了 start 方法,具体流程如下:
- 根据
dyld的Mach-O文件的header判断是否需要对dyld这个Mach-O进行rebase操作
- 初始化
mach,使得dyld可以进行mach通讯。
- 内核将
env指针设置为刚好超出agv数组的末尾;内核将apple指针设置为刚好超出envp数组的末尾
- 栈溢出保护
- 读取
app主二进制文件Mach-O的header来得到偏移量appSlide,然后调用dyld命名空间下的_main方法。
3.3 dyldbootstrap::_main
我们通过搜索来到 dyld.cpp 文件下的 _main 方法:
_main方法 官方的注释如下:
dyld的入口。内核加载了dyld然后跳转到__dyld_start来设置一些寄存器的值然后调用到了这个方法。 返回__dyld_start所跳转到的目标程序的main函数地址。
我们乍一看,这个方法有四五百行,所以我们不能老老实实的一行一行来看,这样太累了。我们应该着重于有注释的地方。
- 我们首先可以看到这里是从环境变量中获取主要可执行文件的
cdHash值。这个哈希值mainExecutableCDHash在后面用来校验dyld3的启动闭包。
- 上图代码作用是追踪
dyld的加载。然后判断当前是否为模拟器环境,如果不是模拟器,则追踪主二进制可执行文件的加载。
- 显示宏定义判断是否为
macOS执行环境,如果是则判断DYLD_ROOT_PATH环境变量是否存在,如果存在,然后判断模拟器是否有自己的dyld,如果有就使用,如果没有,则返回错误信息。
- 打印日志:
dyld 启动开始 - 根据传入
dyldbootstrap::_main方法的参数来设置上下文 - 拾取指向
exec路径的指针 - 从
dyld移除临时apple [0]过渡代码 - 判断
exec路径是否为绝对路径,如果为相对路径,使用cwd转化为绝对路径 - 为了后续的日志打印从
exec路径中取出进程的名称 (strrchr函数是获取第二个参数出现的最后的一个位置,然后返回从这个位置开始到结束的内容) - 根据
App主二进制可执行文件Mach-O的Header的内容配置进程的一些限制条件
- 判断是否为
macOS执行环境,如果是的话,再判断上下文的一些配置属性是否被设置了,如果没有被设置,则再次进行一次setContext上下文配置操作。 - 根据传入的参数
envp检查环境变量 - 默认未初始化的后备路径
- 判断是否为
macOS执行环境,如果是的话,再判断当前app的Mach-O可执行文件是否为iOSMac类型且不为macOS类型的话,则重置上下文的根路径,然后再判断DYLD_FALLBACK_LIBRARY_PATH和DYLD_FALLBACK_FRAMEWORK_PATH这两个环境变量是否都是默认后备路径,如果是的话赋值为受限的后备路径。
- 根据环境变量
DYLD_PRINT_OPTS和DYLD_PRINT_ENV来判断是否需要打印 - 通过当前
app的Mach-O可执行文件的header和ASLR之后的偏移量来获取架构信息。在这里会判断如果是GC的程序则会禁用掉共享缓存。
- 判断共享缓存是否开启,如果开启了就将共享缓存映射到当前进程的逻辑内存空间内
- 检查共享缓存这里会先判断
app的Mach-O二进制可执行文件是否有段覆盖了共享缓存区域,如果覆盖了则禁用共享缓存。但是这里的前提是macOS,在iOS中,共享缓存是必需的。
这里为了方便查看,我们可以折叠一些分支条件。
- 通过共享缓存中的头的版本信息来判断是走
dyld 2还是dyld 3的流程
3.4 dyld3 的处理
- 由于
dyld3会创建一个启动闭包,我们需要来读取它,这里会现在缓存中查找是否有启动闭包的存在,前面我们已经说过了,系统级的app的启动闭包是存在于共享缓存中,而我们自己开发的app的启动闭包是在app安装或者升级的时候构建的,所以这里检查dyld中的缓存是有意义的。
- 宏定义判断代码执行条件为真机。
- 如果
dyld缓存中没有找到启动闭包或者找到了启动闭包但是验证失败(我们最开始提到的cdHash在这里出现了)- 从启动闭包缓存中查找
- 如果还是没有找到,那就创建一个新的启动闭包
- 从启动闭包缓存中查找
- 打印日志信息:
dyld3 启动开始 - 尝试通过启动闭包进行启动
- 如果启动失败,则创建一个新的启动闭包尝试再次启动
- 如果启动成功,由于
start()是以函数指针的方式调用_main方法的返回的指针,需要进行签名。
至此,dyld3 的流程就处理完毕,我们再接着往下分析 dyld2 的流程。
3.5 dyld2 的处理
- 这里会添加
dyld的镜像文件到UUID列表中,主要的目的是启用堆栈的符号化。
reloadAllImages
ImageLoader是一个用于加载可执行文件的基类,它负责链接镜像,但不关心具体文件格式,因为这些都交给子类去实现。每个可执行文件都会对应一个ImageLoader实例。ImageLoaderMachO是用于加载Mach-O格式文件的ImageLoader子类,而ImageLoaderMachOClassic和ImageLoaderMachOCompressed都继承于ImageLoaderMachO,分别用于加载那些__LINKEDIT段为传统格式和压缩格式的Mach-O文件。
接下来就来到重头戏了 reloadAllImages 了:
实例化主程序
这里我们看到有一行代码:
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
显然,在这里我们的主程序被实例化了,我们进入这个方法内部:
这里相当于要为已经映射到主可执行文件中的文件创建一个 ImageLoader*。
从上面代码我们不难看出这里真正执行的逻辑是 ImageLoaderMachO::instantiateMainExecutable 方法:
我们再进入 sniiffLoadCommands 方法内部:
通过注释不难看出:sniiffLoadCommands 会确定此 mach-o 文件是否具有原始的或压缩的 LINKEDIT 以及 mach-o 文件的 segement 的个数。
sniiffLoadCommands 完成后,判断 LINKEDIT 是压缩的格式还是传统格式,然后分别调用对应的 instantiateMainExecutable 方法来实例化主程序。
加载任何插入的动态库
链接库
先是链接主二进制可执行文件,然后链接任何插入的动态库。这里都用到了 link 方法,在这个方法内部会执行递归的 rebase 操作来修正 ASLR 偏移量问题。同时还会有一个 recursiveApplyInterposing 方法来递归的将动态加载的镜像文件插入。
运行所有初始化程序
完成链接之后需要进行初始化了,这里会来到 initializeMainExecutable:
这里注意执行顺序:
- 先为所有插入并链接完成的动态库执行初始化操作
- 然后再为主程序可执行文件执行初始化操作
在 runInitializers 内部我们继续探索到 processInitializers:
然后我们来到 recursiveInitialization:
然后我们来到 notifySingle:
箭头所示的地方是获取镜像文件的真实地址。
我们全局搜索一下 sNotifyObjcInit 可以来到 registerObjCNotifiers:
接着搜索 registerObjCNotifiers:
此时,我们打开 libObjc 的源码可以看到:
上面这一连串的跳转,结果很显然:dyld 注册了回调才使得 libobjc 能知道镜像何时加载完毕。
在 ImageLoader::recursiveInitialization 方法中还有一个 doInitialization 值得注意,这里是真正做初始化操作的地方。
doInitialization 主要有两个操作,一个是 doImageInit,一个是 doModInitFunctions:
doImageInit 内部会通过初始地址 + 偏移量拿到初始化器 func,然后进行签名的验证。验证通过后还要判断初始化器是否在镜像文件中以及 libSystem 库是否已经初始化,最后才执行初始化器。
通知监听 dyld 的 main
一切工作做完后通知监听 dyld 的 main,然后为主二进制可执行文件找到入口,最后对结果进行签名。
四、探索 _objc_init
我们直接通过 LLDB 大法来断点调试 libObjc 中的 _objc_init,然后通过 bt 命令打印出当前的调用堆栈,根据上一节我们探索 dyld 的源码,此刻一切的一切都是那么的清晰明了:
我们可以看到 dyld 的最后一个流程是 doModInitFunctions 方法的执行。
我们打开 libSystem 的源码,全局搜索 libSystem_initializer 可以看到:
然后我们打开 libDispatch 的源码,全局搜索 libdispatch_init 可以看到:
我们再搜索 _os_object_init:
完美~,_objc_init 在这里就被调用了。所以 _objc_init 的流程是
dyld -> libSystem -> libDispatch -> libObc -> _objc_init
五、总结
本文主要探索了 app 启动之后 dyld 的流程,整个分析过程确实比较复杂,但在探索的过程中,我们不仅对底层源码有了新的认知,同时对于优化我们 app 启动也是有很多好处的。下一章,我们会对 objc_init 内部的 map_images 和 load_images 进行更深入的分析,敬请期待~