1. DYLD介绍
在苹果2017年的WWDC大会演讲视频中有关于DYLD的详细介绍。本人也做了一些总结,详细内容如下所示。
1.1 应用启动及其优化
在介绍DYLD之前,你必须明白的几个概念。
-
启动时间(
Startup Time): 启动时间是指在main函数执行之前所用的时间(如果你编写一个应用,你需要做很多的事情,完成之后,需要加载nib文件和执行其他操作,然后再运行UI Application Delegates代码。) -
启动收尾(
Launch Closure): 启动收尾是指启动你的程序所需要的全部信息(比如:程序都使用了哪些dylib,它们的那些偏移位置使用了什么不同的符号,代码签名是什么。)
如何对应用启动进行优化?应用启动优化的原则就是:代码越少,启动速度就越快。具体优化方式如下所示:
-
你应该使用更少的
dylib,减少嵌入的dylib,从时间的角度来看,使用系统库效果更好。 -
应该声明较少的库和方法。
-
减少初始化函数。
苹果建议最好的方式使用更多的Swift代码,原因是Swift从设计上避免许多的陷阱,而在使用C、C++和Objective-C可能会遇到这些陷阱,具体如下所示:
-
Swift没有初始化器。 -
Swift不允许特定类型的未对齐数据结构,这样的结构会延长启动时间。 -
Swift代码更精简,因此性能更好。
我们如何验证初始化函数真的对应用启动速度有影响呢?首先我们创建一个iOS工程,设置其启动页面,如下所示:
在main.m文件中编写如下代码:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
//初始化函数,模拟在初始化函数中进行了网络请求(一个耗时操作),但并未在任何地方调用这个初始化函数
__attribute__((constructor))
void waitForNetworkDebugger (void) {
NSLog(@"发起网络请求!");
for (int i = 0; i < 100000; i++) {
NSLog(@"第%d次", i);
}
NSLog(@"网络请求完毕!");
}
打上如下两个断点:
编译运行程序,我们发现程序首先执行了waitForNetworkDebugger初始化函数,这就表明初始化函数是在main函数之前运行的(为你设置对象),继续运行程序,我们发现启动界面停留了很长时间,但如果我们不知道这个知识点,就在工程中编写了大量的初始化函数,并且在初始化函数中又执行了一些耗时操作,应用启动就变得特别缓慢,用户体验就会变得很糟糕。
苹果在iOS 11的新工具instruments中加入了静态初始化跟踪,可以帮助我们优化缩短应用启动时间,现在Instruments增加这个功能,提供每个静态初始化器的准确时间,使用如下所示:
- 首先,打开
instruments
- 启动一个空模板
- 添加静态初始化工具
- 添加一个
Time Profiler,方便看到运行情况
- 链接真机,点击红色运行按钮,如下图所示:
- 查看
waitForNetworkDebugger耗时数据
因此我们可以快速的发现,在程序启动的过程中,哪些堆栈初始化器导致启动变慢,这涉及多个dylib,包括系统dylib这些系统库会占用很长时间,这是你为它们提供的输入造成的(比如:复杂的nib文件,这依赖于High Sierra和iOS 11的内核和dyld中的新基础结构,你需要新生成程序才能看到这些信息,现在可以捕获大多数初始化器。)
1.2 DYLD发展史
1.2.1 DYLD1
DYLD1(1996 - 2004):包含在NeXTStep 3.3中,此前,NeXT使用静态二进制数据,作用并不是很大,其历史早于标准化POSIX dlopen调用,现在dlopen还存在与某些Unix之中,它们是一些专用扩展,被开发者所采用,NeXTStep具有不同的专用扩展,因此开发者在macOS 10的早期版本上编写第三方包装器,以支持标准Unix软件,问题是这些包装器并不能完美地支持相同的语义,因此可能有一些边界例子不能正常工作,因此会造成运行缓慢,它是在大多数C++动态库被使用的系统之前编写的,C++有许多的特性(比如其初始化器排序方式等),它们在静态环境中工作良好,但是在动态环境中,可能会降低性能,因此大型C++代码库导致动态链接器需要完成大量的工作,速度变慢。
因此为了提升性能,苹果工程师添加了Prebinding(预绑定技术,在macOS Cheetah 10.0 添加)技术,预绑定技术可以为系统中的所有dylib和你的应用程序找到固定地址,动态加载器将会加载这些地址的所有内容,如果成功,将会编辑所有这些二进制数据,以获得所有预计算地址,然后下次当它将所有数据放入相同地址时就不必进行任何其他额外的工作,这会大幅度提高速度,但是这也意味着,每次启动时会编辑你的二进制数据,这并不是很好的做法,至少从安全性来说是如此,
1.2.2 DYLD2.0及DYLD2.x
DYLD 2.0 (2004 - 2007,macOS Tiger的组成部分):是DYLD1的完全重写版本,它增加了如下的内容:
-
正确支持
C++初始化器语义,苹果工程师扩展了mach-o格式,并且更新了DYLD,从而获得高效率的C++库支持。 -
具有完整的本机
dlopen和dlsym实现,具有正确的语义,弃用了旧版的API。
DYLD2的设计目的是为了提高程序加载以及运行速度。因此它仅进行有限的健全性检查,但仍有一些安全遗留问题,因此苹果工程师对一些功能进行改进,提高它在现在平台上的安全性。由于其速度大幅提升,因此可以减少预绑定工作量,不同于编辑你的程序数据,仅编辑系统库,可以仅在软件更新时,做这些事情,因此在软件安装过程中,你可能会看到“优化系统性能”之类的文字,这时就是在更新预绑定,现在DYLD用于所有优化,而在发布DYLD2.0之后又进行大量改进,性能显著提高,DYLD2.x(2007 - 1017)拓展了如下的功能:
1.支持更多架构及平台
-
增加了大量的基础结构和平台(
x86、x86_64、arm、arm64和许多衍生平台) -
推出
iOS、tvOS和watchOS,它们全部都需要新DYLD功能。
2.通过多种方式增强安全性
-
增加代码签名和
ASLR(地址空间配置随机加载),这意味着每次你加载库,它可能位于不同的位置(详情看2016年WWDC大会视频) -
增加了
mach-o文件头中的字段信息,这是重要的边界检查功能,从而可以避免恶意二进制数据的加入。
- 增强了性能,因此可以移除预绑定技术
-
使用共享缓存(最早被引入
iOS 3.1和macOS Snow Leopard中,完全取代预绑定,它是一个单文件,含有大多数系统dylib,由于合并成一个文件,因此可以进行优化),共享缓存在macOS上本地生成运行dyld共享代码,将会大幅优化系统性能,并且带来其他好处。。 -
重新调整所有的文本段和所有数据段重写整个符号表,以减少大小,从而在每个进程中仅挂载少量的区域。
-
它允许我们打包二进制数据段,节省大量的
RAM,它实际上是dylib预链接器。 -
预生成数据结构,供
dyld和ObjC在运行时使用,让我们不必在程序启动时做这些事情,这也会节约更多RAM和时间。
1.2.3 DYLD2.0及DYLD2.x
DYLD3:全新的动态链接器,2017年推出,它完全改变动态链接概念,将成为大多数macOS系统程序的默认设置,2017年Apple OS平台上的所有系统程序都会默认使用它,在未来的Apple OS平台和第三方程序中,它将会全面取代DYLD2。
为什么使用动态链接器?
-
为了性能,尽量提高启动速度,可以帮助我们获得最快的程序启动和运行速度。
-
安全性,虽然在
DYLD2中增加了一些安全特性,但是很难跟随现实情形,增强安全性,那么是否能够进行更积极的安全检查,并且从设计上提高安全性? -
可测试性与可靠性,能否让
dyld变得更容易测试,为此Apple发不了很多不错的测试框架,比如XCTest,它们依赖于动态链接器的底层功能,将它们的库插入进程,因此它们不能用于测试现有的dyld代码,这让苹果程序员难以测试安全性和性能水平。
为了解决以上的问题,苹果程序员对DYLD进行了如下的修整:
-
将大多数
DYLD移出进程,现在只是普通的后台程序,可以使用标准测试工具进行测试,这可以进一步提高速度和性能。 -
另外也运行部分
DYLD驻留在进程中,但是驻留部分尽可能的小,从而减少程序受攻击面积。 -
由于代码速度提升,因此会提高启动速度
1.2.4 DYLD2及DYLD3启动程序流程
1.2.4.1 DYLD2启动程序流程
-
parse mach-o headers(分析mach-o文件,弄清楚应用程序需要哪些库)。 -
find dependencies(这些库也可能需要其他库,进行递归分析,直到获得所有dylib的完整图,普通iOS程序需要3-600个dylib,数据庞大,需要进行大量的处理)。 -
Map mach-o files(映射到所有的mach-o文件,将它们放入地址空间)。 -
Perform symbol lookups(执行符号查找,若你的程序使用printf函数,将会检查printf是否在库系统中,然后找到它的地址,将它复制到你的程序中的函数指针)。 -
Bind and rebase(进行绑定和基址重置,复制这些指针,由于使用随机地址偏移,所有指针必须使用基址)。 -
Run initializers(运行所有的初始化器)。 -
最后准备执行主程序
Main函数。
流程如下图所示:
1.2.4.2 DYLD3启动程序流程
那么DYLD3中是如何加快其速度,会将哪些步骤移出程序?判断依据是什么呢?
Identify security sensitive components(确定安全敏感性组件),从苹果工程师的角度来看,最大的安全隐患之一,分析mach-o文件头和查找依赖关系,因此人们可以使用撰改过的mach-o文件头进行攻击,而且你的程序可能使用@rpaths,它们是搜索路径,通过撰改这些路径或者将库插入到适当的位置,可以破坏程序,因此在后台程序的进程之外,完成所有这些工作。如下图所示:
- 然后也确定大量占用资源的部分,也就是占用缓存的部分,它们是符号查找,因为在给定的库中,除非进行软件更新或者在磁盘上更改库,符号将始终位于库中的相同偏移位置。
确定完这些内容后,我们再来看看它们在dyld3中是怎样做的?
首先将这些部分移到上层,然后向磁盘写入收尾处理。启动收尾处理时启动程序的重要环节,如下图所示:
稍后可以在进程中使用它,DYLD3包含这三个部分:
-
一个进程外
mach-o分析器和编译器。 -
进程内引擎执行启动收尾处理。
-
一个启动收尾缓存服务
大多数程序启动会使用缓存,但始终不需要调用进程外mach-o分析器或编译器。启动收尾比mach-o更简单,它们是内存映射文件,不需要用复杂的方法进行分析,可以简单的验证它们,作用是为了提高速度,让我们来详细看看每个部分.
DYLD3作为进程外mach-o分析器,其分析流程mach-o如下:
-
Resolves all search paths, @rpaths, environment variables(它解析所有的搜索路径、rpaths以及所有环境变量,它们会影响你的启动。 -
Parses the mach-o binaries(分析mach-o二进制数据)。 -
Performs all symbol lookups(执行所有符号查找)。 -
Creates a launch closure with results(利用这些结果创建收尾处理)。 -
Is a normal daemon that can use normal testing infrastructure(它是普通的后台程序可以用来提高测试基础架构的性能)。
DYLD3作为一个小型进程内引擎(这部分驻留在进程中,是你通常会看到的部分),其流程如下所示。
-
Validates launch closure(检查启动收尾处理是否正确)。 -
maps in all dylibs(映射到dylib中,再跳转到main函数)。 -
Applies fixups -
runs initializers(执行所有的初始化器)。 -
jumps to main()(跳转到应用main函数执行代码)。
因此,DYLD3不需要分析mach-o头文件或执行查找符号,不需要做这些事情就可以启动你的应用,由于这些是花费时间的部分,因此可以极大提高程序启动速度,如下图所示:
dyld3作为一个启动收尾缓存服务,其流程如下所示:
-
system app launch closures built into shared cache(系统程序收尾直接加入到共享缓存,我们一使用这个工具在系统中运行和分析每个mach-o文件,我们可以直接将它们放入共享缓存,使它映射到缓存中,所有dylib都是用它来启动,我们甚至不需要打开其他文件)。 -
Third-party app launch closures bulit during install(对于第三方应用,我们在程序安装或系统更新生成你的收尾处理,因为那时系统库已经发生了更改,默认情况下,将在iOS、tvOS和watchOS上生成收尾处理,甚至在程序运行之前)。 -
On macOS the in process engine can call out to a daemon if necessary(在macOS上,由于可以侧向加载程序,如果需要进程内引擎可以在首次启动时RPC到后台程序。在此之后,能够使用缓存的收尾处理,在其它平台上并不需要这样做)
DYLD3有可能出现的问题:
- 首先它完全兼容
dyld 2.x,因此一些现有API会导致你的程序运行变慢或者会在dyld3中使用回退模式。
2. dyld加载应用主流程分析
首先我们创建一个工程,在ViewController.m文件中的类方法load中打上断点,如下所示:
编译运行程序,执行到断点,查看函数调用栈信息,如下所示:
使用bt命令打印堆栈信息,如下所示:
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 9.1
frame #0: 0x0000000105509e8c DemoApp`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:18:1
frame #1: 0x00007fff20181ff2 libobjc.A.dylib`load_images + 1439
frame #2: 0x000000010551fe2c dyld_sim`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 425
frame #3: 0x000000010552eba5 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 437
frame #4: 0x000000010552cec7 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 191
frame #5: 0x000000010552cf68 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #6: 0x000000010552026b dyld_sim`dyld::initializeMainExecutable() + 199
frame #7: 0x0000000105524f56 dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4789
frame #8: 0x000000010551f1c2 dyld_sim`start_sim + 122
frame #9: 0x00000001072cea88 dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2093
frame #10: 0x00000001072cc162 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 1198
frame #11: 0x00000001072c6224 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 450
* frame #12: 0x00000001072c6025 dyld`_dyld_start + 37
最开始调用的函数是_dyld_start,其实这个函数是dyld库中的,因此我们从苹果官网下载dyld库源码,下载完毕之后,使用Xcode打开,然后搜索_dyld_start关键字,找到的内容如下:
实际上_dyld_start是一段汇编代入口地址,在arm架构下,这段汇编代码中,会跳转到C++命名空间dyldbootstrp中的start函数继续执行代码,我们在DYLD中找到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 dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
rebaseDyld(dyldsMachHeader);
// 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(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
在这里主要进行dyld的初始化,函数末尾将会将会调用并返回命名空间dyld下的_mian函数的返回值,这个函数中一共有853行代码,是dyld加载的核心部分,如下所示:
那么我们该从那里下手去分析这个函数中的代码呢?
我们可以根据dyld2以及dyld3的加载流程来大致分析一下代码:
- 检查并准备环境
- 调用
setContext函数解析主二进制的image header等信息。
- 获取指向可执行文件路径的指针
- 检查环境变量(是否应该强制
dyld3,是否应该强制共享缓存,是否有闭包模式等,并根据不同情况做出相应的处理)
- 检查
shared cache是否已经map,没有的话则先执行map shared cache操作。
- 判断是否能使用闭包模式(
dyld3新特性)
- 如果能够使用闭包模式,并且可以使用启动闭包,就会调用
launchWithClosure函数获取result值并返回。
- 如果使用非闭包模式(其实就是
dyld2的流程),首先就要实例化主二进制的image loader(ImageLoader类型),校验主二进制和dyld的版本是否匹配,如下图所示:
- 检查
DYLD_INSERT_LIBRARIES,有的话则加载插入的动态库(实例化image loader),如下图所示:
- 执行
link操作。这个过程比较复杂,会先递归加载依赖的所有动态库(会对依赖库进行排序,被依赖的总是在前面),同时在这阶段将执行符号绑定,以及rebase,binding操作,如下图所示:
- 链接主程序
- 链接插入的所有动态库
- 递归绑定主程序及其依赖的库
- 递归绑定插入的动态库及其依赖的库
- 执行初始化方法。
OC的+load以及C++的constructor方法都会在这个阶段执行,代码如下所示:
- 读取
Mach-O的LC_MAIN段获取程序的入口地址,调用main方法,代码如下所示:
3. 初始化方法流程分析
3.1 loadImages方法调用流程
在之前的内容中,我们有稍微接触到objc源码中的_objc_init函数,但是我们并不知道这个函数如何被调用起来的,现在我们就来探究一下,_objc_init函数代码如下所示:
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
在_objc_init函数中打上断点,编译运行Objc源码程序,运行到断点,查看堆栈信息,如下所示:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001002e7804 libobjc.A.dylib`_objc_init at objc-os.mm:925:9
frame #1: 0x000000010046588f libdispatch.dylib`_os_object_init + 13
frame #2: 0x0000000100476a03 libdispatch.dylib`libdispatch_init + 285
frame #3: 0x00007fff2a6745ff libSystem.B.dylib`libSystem_initializer + 238
frame #4: 0x00000001000316c7 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 535
frame #5: 0x0000000100031ad2 dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #6: 0x000000010002c4b6 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 492
frame #7: 0x000000010002c421 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 343
frame #8: 0x000000010002a26f dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 191
frame #9: 0x000000010002a310 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #10: 0x000000010001686b dyld`dyld::initializeMainExecutable() + 129
frame #11: 0x000000010001ceb2 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 8702
frame #12: 0x0000000100015224 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 450
frame #13: 0x0000000100015025 dyld`_dyld_start + 37
追溯到我们熟悉的流程,我们会发现其实是dyld中dyld命令空间下的initializeMainExecutable函数所调用的,而initializeMainExecutable这个函数就是由dyld命令空间下的_main函数所调用的,这就回到了我们第二部分所探究的第八个流程的内容了,首先我们在dyld源码中看看这个函数:
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
// 运行初始化所有的插入的动态库,所以所插入的动态库的load方法以及构造函数会先调用
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
// 为主可执行文件和它所带来的一切image运行初始化器
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
在这个函数中,调用了runInitializers函数来运行初始化主程序,其代码如下所示:
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.imagesAndPaths[0] = { this, this->getPath() };
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}
在这个函数中,又调用了processInitializers函数,其代码如下所示:
// <rdar://problem/14412057> upward dylib initializers can be run too soon
// To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
// have their initialization postponed until after the recursion through downward dylibs
// has completed.
// 为了处理向上链接而不是向下链接的悬空dylib,所有向上链接的dylib都将其初始化推迟到通过向下链接的dylib的递归完成之后。
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
// 对images列表中的所有image调用递归init,构建一个未初始化向上依赖项的新列表。
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
在这个函数中,又调用了recursiveInitialization函数,而这个函数中的代码如下所示:
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
// 首先初始化轻量级的库
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order 记录终止订单
if ( this->needsTermination() )
context.terminationRecorder(this);
// let objc know we are about to initialize this image
// 让objc知道我们将要初始化这个图像
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image 初始化这个image
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
// 让所有的库都知道我们初始化了这个image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
//这个Image没有初始化完成
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock();
}
首先我们可以清楚的看到这个函数会进行递归调用,也就是会递归初始化主程序所依赖的库,然后context又调用了notifySingle发送了一个通知,因此我们需要知道notifySingle做了什么,因此全局搜索notifySingle,发现是在setContext这个函数中被赋值了,如下图所示:
而notifySingle其实就是一个函数地址,而这个函数中的代码如下所示:
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
//dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
if ( handlers != NULL ) {
dyld_image_info info;
info.imageLoadAddress = image->machHeader();
info.imageFilePath = image->getRealPath();
info.imageFileModDate = image->lastModified();
for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
const char* result = (*it)(state, 1, &info);
if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
//fprintf(stderr, " image rejected by handler=%p\n", *it);
// make copy of thrown string so that later catch clauses can free it
const char* str = strdup(result);
throw str;
}
}
}
if ( state == dyld_image_state_mapped ) {
// <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
// <rdar://problem/50432671> Include UUIDs for shared cache dylibs in all image info when using private mapped shared caches
if (!image->inSharedCache()
|| (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {
dyld_uuid_info info;
if ( image->getUUID(info.imageUUID) ) {
info.imageLoadAddress = image->machHeader();
addNonSharedCacheImageUUID(info);
}
}
}
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
uint64_t t1 = mach_absolute_time();
uint64_t t2 = mach_absolute_time();
uint64_t timeInObjC = t1-t0;
uint64_t emptyTime = (t2-t1)*100;
if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
timingInfo->addTime(image->getShortName(), timeInObjC);
}
}
// mach message csdlc about dynamically unloaded images
if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
notifyKernel(*image, false);
const struct mach_header* loadAddress[] = { image->machHeader() };
const char* loadPath[] = { image->getPath() };
notifyMonitoringDyld(true, 1, loadAddress, loadPath);
}
}
在这个函数中,我们又该如何分析呢?这么多代码到底哪里是重点呢?首先我们来看看这个函数中都调用了哪些方法,如下图所示:
然后当我们全局搜索这些方法时,发现sNotifyObjCInit在registerObjCNotifiers这个函数中被赋值,如下所示:
// _dyld_objc_notify_init
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}
// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
// 在所有已经初始化的图像上调用'init'函数(在libSystem之下)
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}
}
而registerObjCNotifiers又在_dyld_objc_notify_register函数中被调用,代码如下所示:
// _dyld_objc_notify_register
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
而_dyld_objc_notify_register这个函数是我们所熟悉的,因为之前我们在objC源码中_objc_init函数中见过这个函数的调用,而此时在notifySingle函数中sNotifyObjCInit的调用实际上就是objC源码中load_images函数的调用,而load_images所做的操作就是调用所有OC类以及分类中所有的类方法load,如下所示:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
call_load_methods函数中代码如下所示:
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
而其中的call_class_loads函数就是对所有类的类方法load进行调用,call_category_loads函数就是对所有分类中的类方法load进行调用,代码如下所示:
typedef void(*load_method_t)(id, SEL);
// call_class_loads函数
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
// call_category_loads函数
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
....
}
以上的流程就是objC源码中loadImages函数被调用的原因,而_objc_init这个函数是在dylib库libdispatch中的函数_os_object_init所调用的(由之前所查看_objc_init函数调用的堆栈信息可知),其代码如下所示:
void
_os_object_init(void)
{
_objc_init();
Block_callbacks_RR callbacks = {
sizeof(Block_callbacks_RR),
(void (*)(const void *))&objc_retain,
(void (*)(const void *))&objc_release,
(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}
而_os_object_init这个函数又是库libdispatch的函数libdispatch_init所调用的
void
libdispatch_init(void)
{
dispatch_assert(sizeof(struct dispatch_apply_s) <=
DISPATCH_CONTINUATION_SIZE);
...
...
...
_os_object_init();
_voucher_init();
_dispatch_introspection_init();
}
而函数libdispatch_init是初始化器_libdispatch_init函数所调用的,如下所示:
DISPATCH_NOTHROW __attribute__((constructor))
void
_libdispatch_init(void);
DISPATCH_NOTHROW
void
_libdispatch_init(void)
{
libdispatch_init();
}
而_libdispatch_init函数是在库libSystem的初始化器函数 中被调用的(由之前所查看_objc_init函数调用的堆栈信息可知),部分代码如下所示:
extern void libdispatch_init(void); // from libdispatch.dylib
__attribute__((constructor))
static void
libSystem_initializer(int argc,
const char* argv[],
const char* envp[],
const char* apple[],
const struct ProgramVars* vars)
{
static const struct _libkernel_functions libkernel_funcs = {
.version = 4,
#if !TARGET_OS_DRIVERKIT
.dlsym = dlsym,
#endif
.malloc = malloc,
.free = free,
.realloc = realloc,
._pthread_exit_if_canceled = _pthread_exit_if_canceled,
// V2 functions (removed)
// V3 functions
.pthread_clear_qos_tsd = _pthread_clear_qos_tsd,
// V4 functions
.pthread_current_stack_contains_np = pthread_current_stack_contains_np,
};
...
...
...
libdispatch_init();
_libSystem_ktrace_init_func(LIBDISPATCH);
...
...
...
}
而libSystem初始化函数libSystem_initializer是在dyld库中的函数doModInitFunctions调用的,其代码如下所示:
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
// <rdar://problem/23929217> Ensure __mod_init_func section is within segment
if ( (sect->addr < seg->vmaddr) || (sect->addr+sect->size > seg->vmaddr+seg->vmsize) || (sect->addr+sect->size < sect->addr) )
dyld::throwf("__mod_init_funcs section has malformed address range for %s\n", this->getPath());
for (size_t j=0; j < count; ++j) {
Initializer func = inits[j];
// <rdar://problem/8543820&9228031> verify initializers are in image
//验证初始化方法是否在image中
if ( ! this->containsAddress(stripPointer((void*)func)) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
if ( ! dyld::gProcessInfo->libSystemInitialized ) {
// <rdar://problem/17973316> libSystem initializer must run first
// libSystem库中的初始化器必须是第一个被调用的
const char* installPath = getInstallPath();
if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
}
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
// now safe to use malloc() and other calls in libSystem.dylib
dyld::gProcessInfo->libSystemInitialized = true;
}
}
}
else if ( type == S_INIT_FUNC_OFFSETS ) {
const uint32_t* inits = (uint32_t*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uint32_t);
// Ensure section is within segment
if ( (sect->addr < seg->vmaddr) || (sect->addr+sect->size > seg->vmaddr+seg->vmsize) || (sect->addr+sect->size < sect->addr) )
dyld::throwf("__init_offsets section has malformed address range for %s\n", this->getPath());
if ( seg->initprot & VM_PROT_WRITE )
dyld::throwf("__init_offsets section is not in read-only segment %s\n", this->getPath());
for (size_t j=0; j < count; ++j) {
uint32_t funcOffset = inits[j];
// verify initializers are in image
if ( ! this->containsAddress((uint8_t*)this->machHeader() + funcOffset) ) {
dyld::throwf("initializer function offset 0x%08X not in mapped image for %s\n", funcOffset, this->getPath());
}
if ( ! dyld::gProcessInfo->libSystemInitialized ) {
// <rdar://problem/17973316> libSystem initializer must run first
const char* installPath = getInstallPath();
if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
}
Initializer func = (Initializer)((uint8_t*)this->machHeader() + funcOffset);
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
#if __has_feature(ptrauth_calls)
func = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)func, ptrauth_key_asia, 0);
#endif
bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
// now safe to use malloc() and other calls in libSystem.dylib
dyld::gProcessInfo->libSystemInitialized = true;
}
}
}
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
这个函数中的代码也是相当长的,但是我们关注的重点在哪里呢?其实这个函数的作用就是用来调用主程序以及主程序中所依赖的库的所有的初始化构造函数,但是在调用的过程中,必须要先保证最下层被依赖的库libSystem首先被初始化,然后再依次向上初始化所依赖的库,直到初始化主程序自身所有的初始化构造函数,而doModInitFunctions函数又是在doInitialization函数中被调用的,其代码如下所示:
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
而当doInitialization函数又是在recursiveInitialization函数中所调用的,也是在context.notifySingle调用之后所调用的,因此在这里就形成了一个完美的函数调用闭环。
但此时此刻你可能会有一个很大的疑惑,那就是如果你在主工程中同时写了一个构造函数以及在类中重写了load类方法,你会发现在程序运行的过程中,会先调用load类方法,然后调用构造函数,最后调用main函数,代码以及运行结果如下所示:
@interface Person : NSObject
@end
@implementation Person
+ (void)load {
NSLog(@"执行load方法");
}
@end
__attribute__((constructor))
void initFun() {
NSLog(@"执行构造方法");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"执行main函数");
}
return 0;
}
执行输出结果:
而按照之前我们分析的流程结果,如果想要在context的notifySingle函数中调用sNotifyObjCInit,就必须先要调用ObjC库的初始化函数_objc_init函数对sNotifyObjCInit进行赋值,而要调用ObjC库的_objc_init初始化函数,就必须先要调用dyld的doInitialization函数,但是在dyld的代码中库的初始化函数doInitialization是在notifySingle调用之后的,那这样照成的结果不应该是主程序的初始化方法早于load方法进行调用吗?但是实际运行的过程中其实是相反的结果,这是什么原因呢?
其实你应该要知道的是,这个是一个递归初始化的过程,要先对主工程所依赖的库先进行递归初始化,而最底层所依赖的库就是libSystem,而在调用libSystem的初始化函数时对libDispath这个库进行初始化,然后libDispath这个库函数的初始化又会调用objc这个库的初始化函数,最后执行主程序的初始化流程,而在执行主程序的初始化流程的notifySingle调用时,此时此刻objc的初始化函数已经执行过了,而dyld的sNotifyObjCInit以及被赋值了,并且主工程中OC类也已经加载完毕(详情请看 map_images函数调用流程),此时,就可以对主工程中所有的OC类的load方法进行调用了,然后再执行doInitialization对主工程的构造函数进行调用。
3.2 map_images函数调用流程
如果你详细探究过map_images这个函数,你会发现这个函数是用来加载OC类的,但是这个函数是在什么时候被调用的呢?
首先,我们知道map_images这个函数是通过调用dyld中函数_dyld_objc_notify_register并且以函数指针形式传递给dyld的,这里与传递load_images函数有所不同,为什么要这样做呢,原因就在于OC类加载的加载是十分关键而重要的,所以必须要保证map_images函数的调用时同步的,但是传递到dyld又是如何被调用的呢?因此我们来看看_dyld_objc_notify_register函数的源码,上面的源码中又是调用了registerObjCNotifiers函数,而在registerObjCNotifiers函数中是将指向map_image函数指针的值赋值给了sNotifyObjCMapped这个静态变量进行了存储,因此我们只需要看看在哪里对这个静态变量进行了调用即可,全局搜索sNotifyObjCMapped,代码如下:
static void notifyBatchPartial(dyld_image_states state, bool orLater, dyld_image_state_change_handler onlyHandler, bool preflightOnly, bool onlyObjCMappedNotification)
{
std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sBatchHandlers);
if ( (handlers != NULL) || ((state == dyld_image_state_bound) && (sNotifyObjCMapped != NULL)) ) {
...
...
...
// tell objc about new images
if ( (onlyHandler == NULL) && ((state == dyld_image_state_bound) || (orLater && (dyld_image_state_bound > state))) && (sNotifyObjCMapped != NULL) ) {
const char* paths[imageCount];
const mach_header* mhs[imageCount];
unsigned objcImageCount = 0;
for (int i=0; i < imageCount; ++i) {
ImageLoader* image = findImageByMachHeader(infos[i].imageLoadAddress);
bool hasObjC = false;
if ( image != NULL ) {
if ( image->objCMappedNotified() )
continue;
hasObjC = image->notifyObjC();
}
#if SUPPORT_ACCELERATE_TABLES
else if ( sAllCacheImagesProxy != NULL ) {
const mach_header* mh;
const char* path;
unsigned index;
if ( sAllCacheImagesProxy->addressInCache(infos[i].imageLoadAddress, &mh, &path, &index) ) {
hasObjC = (mh->flags & MH_HAS_OBJC);
}
}
#endif
if ( hasObjC ) {
paths[objcImageCount] = infos[i].imageFilePath;
mhs[objcImageCount] = infos[i].imageLoadAddress;
++objcImageCount;
if ( image != NULL )
image->setObjCMappedNotified();
}
}
if ( objcImageCount != 0 ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_MAP, 0, 0, 0);
uint64_t t0 = mach_absolute_time();
(*sNotifyObjCMapped)(objcImageCount, paths, mhs);
uint64_t t1 = mach_absolute_time();
ImageLoader::fgTotalObjCSetupTime += (t1-t0);
}
}
}
...
...
...
}
}
然后我们再来搜索看看notifyBatchPartial这个函数是在哪里调用的,代码如下图所示:
原来在给sNotifyObjCMapped赋值完毕之后就对其进行了调用,这个时候就会执行objc中的map_images函数来进行OC类的加载,关于OC类的加载在下一篇文章我们会详细进行分析探讨。