+load和main()谁先调用?有经验的iOSer们会毫不犹豫的回答出来是load方法,但为什么是load方法呢?今天我们来探讨一下底层的原理
新建一个项目,在AppDelegate里添加load方法,打上一个断点就会看到如下图所示的调用堆栈,如果嫌左侧太长了看不全,也可以在控制台输入bt指令查看调用堆栈
从调用堆栈中我们可以看到,程序由dyld的_dyld_start
函数开始,一步一步的层层调用,最终到了我们Demo程序的[AppDelegate load]
方法中,看上去这个dyld也是一个程序,就是这个dyld程序在启动我们的APP
简介
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。WWDC从2016,2017到2019都有session对APP启动的过程以及如何优化做过介绍,直到WWDC2019(iOS 13)才把Dyld3开放给所有APP,在iOS 13系统中,iOS将全面采用新的dyld 3以替代之前版本的dyld 2。 因为dyld 3完全兼容dyld 2,API接口是一样的,所以在大部分情况下,开发者不需要做额外的适配就能平滑过渡。现在网上大多数关于dyld的文章介绍都是基于dyld2版本的,虽然现在已经是dyld3版本了,但dyld3也并非是对dyld2的完全重构,dyld2里的主要流程在dyld3里面依然存在,所以我这篇文章也会先介绍一下dyld2中的9个主要流程,最后再简单介绍一下dyld3做了哪些优化
dyld下载地址:opensource.apple.com/tarballs/dy…
从调用堆栈跟踪
我们先跟着刚刚的调用堆栈来跟踪一遍,首先打开我们刚刚下载的dyld源码,搜索我们刚刚在调用堆栈里看到的最外层的调用_dyld_start
_dyld_start
从截图中可以看到,搜索结果的第一部分和第二部分,都不可能是我们想要找的东西,第一部分.xcconfig像是配置文件,第二部分都是注释;那么肯定只有第三部分了,其实一开始看到这个第三部分dyldStartup.s文件内心是崩溃的...(作为非科班iOSer,虽然学过C,Objective-C,Swift也了解过一点点C++,但是这个.s文件真不认识)点开这个.s文件后,看到一堆指令,猜测应该是汇编代码了,这下头更大了,不过好在是汇编,估计大家都挺难懂的,所以苹果的注释给的也很到位,根据搜索结果综合注释来看,找到arm64架构并且不是模拟器的这部分也还算简单,然后看到bl指令上面有一段注释,跟我们刚刚调用堆栈里面的第8帧是一模一样的就能猜到个大概了...dyld程序由_dyld_start开始,执行到这个bl指令后开始调用start函数了,那么接下来就是搜索这个start函数在哪了
start
这个start一开始还不好搜,直接搜start会发现有3125个结果在363个文件里...作者表示看到这个结果头很大,于是想了个办法,我要找的是个函数的声明,那么后面必定紧跟着一个(符号,重新搜索一番后发现好了很多,只有173个结果在118个文件了;头虽然没那么大了,但是这173个结果要一个个去找去看,也还是蛮费时间的...最后灵机一动,尝试在start(前面加一个空格,好家伙,这不就出现了么,结合一下注释,还有函数的参数和调用堆栈第8帧里一模一样的就确定了;如果是有C++基础的同学应该就更加好找了dyldbootstrap是命名空间直接搜就会找到一份文件,再在该文件里搜start(很快就找到了
函数的return的前面一行有一个appsSlide变量,这个变量是ASLR地址空间配置随机加载技术的应用,这是是一种防范内存损坏漏洞被利用的计算机安全技术
这个start函数里面的最后一行代码调用了_main()函数,这个比较舒服,不需要我们再去找了,直接按住command然后左键点击就可以跳转到对应的实现代码了
_main
这个_main函数就是启动我们APP的关键代码,从它的行数就可以看出来它的分量了,从6455行到7303一共848行(这里就不贴它的全部源码了)好家伙,还是头一回看到一个C函数写这么长的,本来是想吐槽一下的,但一想到这是苹果的工程师写的底层代码,咱还是老老实实看源码...这部分的代码就是今天重点中的重点,我们在下一部分重点讲这个函数,现在还是接着调用堆栈继续往后面走,后面的几个都挺好找的
initializeMainExecutable
这个initializeMainExecutable
函数在_main
里面调用了两次,不过是分不同架构的,也是可以直接command加左键定位到函数实现的,从这个函数里的注释中可以看到,是先执行的所有插入的库的initialzers方法,再执行我们主程序的initialzers方法的,这也说明了我们写的Framework中的load方法会比我们主程序的load方法先执行;如果不认为run initialzers就是调用load方法,可以跟着调用堆栈流程走完,你就会知道到底是不是了
runInitializers
这个runInitializers
函数也是在上面initializeMainExecutable
函数里面调用了两次,从两次调用的注释来看,上面调用的是所有插入的动态库的初始化方法,而下面的才是我们主程序调用初始化方法,所以在[AppDelegate laod]
方法中打的断点卡住的应该是下面的这段代码;这个函数也比较好找,直接搜runInitializers(只有12个结果在5个文件里,载结合它前面的ImageLoader作用域,函数的参数,很快就能找到它的实现
processInitializers
结合调用堆栈,找到runInitializers
里面的processInitializers
函数实现很容易,找到processInitializers
函数的实现也很简单,可以直接command加左键点击就到了
recursiveInitialization
同样是在ImageLoader.cpp文件内,processInitializers
就能直接command加左键定位到实现代码,而recursiveInitialization
却不可以,不知道为什么,不过也没什么大问题,recursiveInitialization
在当前文件一搜就找到实现了
这里有个小细节可以说一下,在第一次content.notifySingle()之后有一个doInitialization
函数,这个函数
里面又会有两个初始化函数
doInitialization()内部首先调用doImageInit来执行镜像的初始化函数,也就是LC_ROUTINES_COMMAND中记录的函数。
再执行doModInitFunctions()方法来解析并执行_DATA_,__mod_init_func
这个section中保存的函数。使用__attribute__((constructor))
开头的C函数会保存在这里面,如图所示:
notifySingle
notifySingle
的调用同样是在上一个函数recursiveInitialization
的实现里面,但是notifySingle
的实现结合调用堆栈它前面的作用域来看,不在ImageLoader里面,那就直接全局搜索notifySingle(,也比较容易找
接下来,由notifySingle
到load_images
会发现调用的地方已经不是在dyld了...那么如何实现代码的执行从一个程序跳到另一个程序呢?有很多种办法,通知,代理,block,函数作为参数传递,我们仔细观察一下notifySingle
里面有没有以上任何一种,会发现下面这里有一个不太一样的地方sNotifyObjCInit
那接下来就看看这个sNotifyObjCInit
变量是在哪里被赋值的,搜索一番后发现在这里被赋值了
紧接着搜一搜这个registerObjCNotifiers
在哪里被调用了
搜索一番后发现这里是dyld提供的对外部的接口...那就说明我们在dyld里面应该是找不到这个调用的地方了,不过至少我们找到了这个对外的接口函数_dyld_objc_notify_register
,这个时候,我们可以回到最开始新建的项目中去,下一个_dyld_objc_notify_register
的符号断点
会发现是libobjc.A.dylib的_objc_init里面调用了我们的_dyld_objc_notify_register
函数,这个libobjc.A.dylib我们不是头一回看到了,前面的调用堆栈第1帧也是在libobjc.A.dylib的load_images函数,那么这个libobjc.A.dylib到底是什么库呢?其实这个libobjc.A.dylib就是我们的Objective-C的运行时库,好消息是这个库苹果依旧开源了出来,可以免费供大家学习,我这里下载的是objc4-818.2
OC运行时库下载:opensource.apple.com/tarballs/ob…
打开下载的objc4源码,找到Products目录,是不是可以看到一个非常眼熟的东西
这样看来我们真的找对地方了,那就直接全局搜索我们刚刚获取到的_dyld_objc_notify_register
函数,看看是不是_objc_init
里面调用了它
果然如此,看到这里,就会发现dyld里面的sNotifyObjCInit
变量是被赋值了一个load_images
的函数,到这里就完全能解释的通,从调用堆栈的第2帧到第1帧的执行了,这个load_images
可以直接按住command点击定位到实现代码
load_images
接下来就是看怎么从libobjc.A.dylib的load_images函数到我们的断点[AppDelegate load]
方法的了
很明显,我们需要查看call_load_methods
函数
在这个函数里,我们就可以明显的看到先是调用所有类Class的load方法,然后再调用所有分类category中的load方法,查看call_class_loads
的实现
到这里,我们断点卡住的所有调用堆栈就全部跟踪完毕了
需要了解的是,这个时候我们依然还在dyld的_main
函数里面,连_main
里面的initializeMainExecutable
都没有执行完...而我们主程序的main()是在什么时候调用的呢?dyld的_main
函数的返回值result就是我们主程序的入口,_main
执行完毕之后,会把返回值返回到start
函数,start
函数又会把返回值返回到我们的最初的入口_dyld_start
里面的那条bl
指令后,x0
就是返回的我们主程序入口,接下来一个mov x16,x0
看注释也知道是将主程序入口地址保存到x16
了,再搜索一下x16
发现后面基本都是各种情况下的br
或者braaz
到x16
,就是跳转到我们的主程序了,所以我们APP的main函数远远晚于load方法的调用
dyld的_main
上面调用堆栈里这么多函数里面,最复杂最长的就是这个_main
了,其实这个函数里面才是dyld启动我们APP的主要流程,但是这个函数实在太长了,我们将这个函数分成9个主要的部分,我向来讨厌在文章里面贴那种几页几页都翻不完的代码块的,所以下面的介绍都尽量精简
1.获取当前程序架构,设置上下文信息
getHostInfo()获取当前程序架构
getHostInfo(mainExecutableMH, mainExecutableSlide);
接着调用setContext()设置上下文信息,包括一些回调函数、参数、标志信息等。设置的回调函数都是dyld模块自身实现的,如loadLibrary()函数实际调用的是libraryLocator(),负责加载动态库。代码片断如下:
static void setContext(const macho_header* mainExecutableMH, int argc, const char* argv[], const char* envp[], const char* apple[])
{
gLinkContext.loadLibrary = &libraryLocator;
gLinkContext.terminationRecorder = &terminationRecorder;
......
2.配置进程是否受限,检查环境变量
configureProcessRestrictions()用来配置进程是否受限
checkEnvironmentVariables()检查环境变量
细心的读者可能会注意到,整个过程中有一些DYLD_PRINT_开头的环境变量,比如:
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
如果在Xcode中配置了这些环境变量,就会在我们的控制台中打印相关的信息:
除了上面的两个外,我下面以另外一个打印启动时间的环境变量DYLD_PRINT_STATISTICS_DETAILS
为例,这个应该可以作为优化启动时间的参考数据
3.加载共享缓存
这里先说明一下,iOS的共享缓存机制:在iOS系统中,每个程序依赖的动态库都需要通过dyld一个个加载到内存,然而,很多系统库基本上是每个程序都会用到的(比如UIKit,Foundation...),如果每个程序启动运行的时候都重复的去加载一次,势必会造成运行缓慢,不必要的内存消耗,为了优化启动速度和节约内存消耗,共享缓存机制就出现了;这里还要说一句,在iOS中,只有系统库才能称为真正意义上的动态库,我们普通开发者开发的各种格式的库,都只能是静态库;所有默认的动态链接库被合并成一个大的缓存文件,放在/System/Library/Caches/com.apple.dyld/
目录下,按不同的架构分别保存,想要分析某个系统库,可以从dyld_shared_cache里将原始的二进制文件提取出来;感兴趣的可以根据我的第一篇参考文章上的步骤去尝试一下
这一步先调用checkSharedRegionDisable()检查共享缓存是否禁用。该函数的iOS实现部分仅有一句注释,从注释我们可以推断iOS必须开启共享缓存才能正常工作,代码如下: 接下来调用mapSharedCache()加载共享缓存,而mapSharedCache()里面实则是调用了loadDyldCache(),从代码可以看出,共享缓存加载又分为三种情况:
- 仅加载到当前进程,调用mapCachePrivate()。
- 共享缓存已加载,不做任何处理。
- 当前进程首次加载共享缓存,调用mapCacheSystemWide()。
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->errorMessage = nullptr;
#if TARGET_OS_SIMULATOR
// simulator only supports mmap()ing cache privately into process
return mapCachePrivate(options, results);
#else
if ( options.forcePrivate ) {
// mmap cache into this process only
return mapCachePrivate(options, results);
}
else {
// fast path: when cache is already mapped into shared region
bool hasError = false;
if ( reuseExistingCache(options, results) ) {
hasError = (results->errorMessage != nullptr);
} else {
// slow path: this is first process to load cache
hasError = mapCacheSystemWide(options, results);
}
return hasError;
}
#endif
}
mapCachePrivate()、mapCacheSystemWide()里面就是具体的共享缓存解析逻辑,感兴趣的读者可以详细分析。
4.为主程序实例化ImageLoader
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
分析一下函数的名称以及参数,从已加载的镜像实例化,因为传入的是我们的主程序的MachO头,主程序Slide和路径,所以实例化出来的就是我们的主程序,代码如下:
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
ImageLoaderMachO::instantiateMainExecutable()函数里面首先会调用sniffLoadCommands()函数来获取一些数据,包括:
compressed
如果若Mach-O存在LC_DYLD_INFO和LC_DYLD_INFO_ONLY加载命令或LC_DYLD_CHAINED_FIXUPS加载命令,则说明是压缩类型的Mach-O,代码片段如下:
switch (cmd->cmd) {
case LC_DYLD_INFO:
case LC_DYLD_INFO_ONLY:
if ( cmd->cmdsize != sizeof(dyld_info_command) )
throw "malformed mach-o image: LC_DYLD_INFO size wrong";
dyldInfoCmd = (struct dyld_info_command*)cmd;
*compressed = true;
break;
case LC_DYLD_CHAINED_FIXUPS:
if ( cmd->cmdsize != sizeof(linkedit_data_command) )
throw "malformed mach-o image: LC_DYLD_CHAINED_FIXUPS size wrong";
chainedFixupsCmd = (struct linkedit_data_command*)cmd;
*compressed = true;
break;
segCount
根据 LC_SEGMENT_COMMAND 加载命令来统计段数量,这里抛出的错误日志也说明了段的数量是不能超过255个,代码片段如下:
if ( *segCount > 255 )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
libCount
根据 LC_LOAD_DYLIB、LC_LOAD_WEAK_DYLIB、LC_REEXPORT_DYLIB、LC_LOAD_UPWARD_DYLIB 这几个加载命令来统计库的数量,库的数量不能超过4095个。代码片段如下:
case LC_LOAD_DYLIB:
case LC_LOAD_WEAK_DYLIB:
case LC_REEXPORT_DYLIB:
case LC_LOAD_UPWARD_DYLIB:
*libCount += 1;
......
if ( *libCount > 4095 )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
codeSigCmd
通过解析LC_CODE_SIGNATURE来获取代码签名加载命令,代码片段如下:
case LC_CODE_SIGNATURE:
if ( cmd->cmdsize != sizeof(linkedit_data_command) )
throw "malformed mach-o image: LC_CODE_SIGNATURE size wrong";
// <rdar://problem/22799652> only support one LC_CODE_SIGNATURE per image
if ( *codeSigCmd != NULL )
throw "malformed mach-o image: multiple LC_CODE_SIGNATURE load commands";
*codeSigCmd = (struct linkedit_data_command*)cmd;
break;
encryptCmd
通过LC_ENCRYPTION_INFO和LC_ENCRYPTION_INFO_64来获取段的加密信息,代码片段如下:
case LC_ENCRYPTION_INFO:
if ( cmd->cmdsize != sizeof(encryption_info_command) )
throw "malformed mach-o image: LC_ENCRYPTION_INFO size wrong";
// <rdar://problem/22799652> only support one LC_ENCRYPTION_INFO per image
if ( *encryptCmd != NULL )
throw "malformed mach-o image: multiple LC_ENCRYPTION_INFO load commands";
*encryptCmd = (encryption_info_command*)cmd;
break;
case LC_ENCRYPTION_INFO_64:
if ( cmd->cmdsize != sizeof(encryption_info_command_64) )
throw "malformed mach-o image: LC_ENCRYPTION_INFO_64 size wrong";
// <rdar://problem/22799652> only support one LC_ENCRYPTION_INFO_64 per image
if ( *encryptCmd != NULL )
throw "malformed mach-o image: multiple LC_ENCRYPTION_INFO_64 load commands";
*encryptCmd = (encryption_info_command*)cmd;
break;
ImageLoader是抽象类,其子类负责把Mach-O文件实例化为image,当sniffLoadCommands()解析完以后,根据compressed的值来决定调用哪个子类进行实例化,代码如下:
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}
在完成实例化之后,将返回的image加入到sAllImages加入到全局镜像列表,并将image映射到申请的内存中;至此,初始化主程序这一步就完成了
5.加载所有插入的库
这一步是加载环境变量DYLD_INSERT_LIBRARIES中配置的动态库,先判断环境变量DYLD_INSERT_LIBRARIES中是否存在要加载的动态库,如果存在则调用loadInsertedDylib()依次加载,代码如下:
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
6.链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
这一步调用link()函数将实例化后的主程序进行动态修正,让二进制变为可正常执行的状态。link()函数内部调用了ImageLoader::link()函数,从源代码可以看到,这一步主要做了以下几个事情:
- recursiveLoadLibraries() 根据LC_LOAD_DYLIB加载命令把所有依赖库加载进内存。
- recursiveUpdateDepth() 递归刷新依赖库的层级。
- recursiveRebase() 由于ASLR的存在,必须递归对主程序以及依赖库进行重定位操作。
- recursiveBind() 把主程序二进制和依赖进来的动态库全部执行符号表绑定。
- weakBind() 如果链接的不是主程序二进制的话,会在此时执行弱符号绑定,主程序二进制则在link()完后再执行弱符号绑定,后面会进行分析。
- recursiveGetDOFSections()、context.registerDOFs() 注册DOF(DTrace Object Format)节。
7.链接所有插入的库
这一步与链接主程序一样,将前面调用addImage()函数保存在sAllImages中的动态库列表循环取出并调用link()进行链接,需要注意的是,sAllImages中保存的第一项是主程序的镜像,所以要从i+1的位置开始,取到的才是动态库的ImageLoader:
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing(gLinkContext);
}
}
}
8.执行初始化方法initializeMainExecutable
initializeMainExecutable();
这个函数在我们刚刚的调用堆栈流程跟踪里面讲到过,我们Objective-C对象的load方法,库中的load方法,还有C++的初始化方法都在这里面被执行了
9.查找主程序入口并返回
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
if ( result != 0 ) {
// main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
*startGlue = 0;
}
调用getEntryFromLC_MAIN(),从Load Command读取LC_MAIN入口;如果没有LC_MAIN入口,就读取LC_UNIXTHREAD(),然后返回给start
函数,再返回到_dyld_start
走完剩下的汇编代码,可以看到最后的汇编代码跳转到了我们程序的入口jump to the program's entry point
dyld2和dyld3
在 iOS 13 之前,所有的第三方 App 都是通过 dyld 2 来启动 App 的,主要过程就是上面所讲的9大步骤
上面的所有过程都发生在 App 启动时,包含了大量的计算和I/O,所以苹果开发团队为了加快启动速度,在 WWDC2017 - 413 - App Startup Time: Past, Present, and Future 上正式提出了 dyld3。
dyld 3并不是WWDC19推出来的新技术,早在2017年就被引入至iOS 11,当时主要用来优化系统库。现在,在iOS 13中它也将用于启动第三方APP。dyld 3最大的特点就是部分是进程外的且有缓存的,在打开APP时,实际上已经有不少工作都完成了。
dyld 3包含三个组件
本APP进程外的Mach-O分析器/编译器;
在dyld 2的加载流程中,Parse mach-o headers和Find Dependencies存在安全风险(可以通过修改mach-o header及添加非法@rpath进行攻击),而Perform symbol lookups会耗费较多的CPU时间,因为一个库文件不变时,符号将始终位于库中相同的偏移位置,这两部分在dyld 3中将采用提前写入把结果数据缓存成文件的方式构成一个”lauch closure“(可以理解为缓存文件)。
它处理了所有可能影响启动速度的 search path,@rpaths 和环境变量;它解析 mach-o 二进制文件,分析其依赖的动态库,并且完成了所有符号查找的工作;最后它将这些工作的结果创建成了启动闭包,写入缓存,这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。 这是一个普通的 daemon 进程,可以使用通常的测试架构。 out-of-process是一个普通的后台守护程序,因为从各个APP进程抽离出来了,可以提高dyld3的可测试性。
本进程内执行”lauch closure“的引擎;
验证”lauch closures“是否正确,把dylib映射到APP进程的地址空间里,然后跳转到main函数。此时,它不再需要分析mach-o header和执行符号查找,节省了不少时间。
”lauch closure“的缓存:
iOS操作系统内置APP的”lauch closure“直接内置在shared cache共享缓存中,我们甚至不需要打开一个单独的文件;而对于第三方APP,将在APP安装或更新版本时(或者操作系统升级时?)生成lauch closure启动闭包,因为那时候的系统库已经发生更改。这样就能保证”lauch closure“总是在APP打开之前准备好。启动闭包会被写到到一个文件里,下次启动则直接读取和验证这个文件。
总结
dyld 3 把很多耗时的查找、计算和 I/O 的事前都预先处理好了,这使得启动速度有了很大的提升。dyld3在_main
函数里面和dyld2最大的不同之处在于,在dyld2的第三步和第四步之间,插入了使用Closure启动的逻辑。在最新的dyld源码里面可以看到,在第三步加载共享缓存之后,会判断sClosureMode
模式,并尝试通过Closure的方式启动,如果启动成功了就直接return了,后面的代码就不执行了,当然launchWithClosure()
里面会有dyld3的新处理逻辑,感兴趣的同学可以自行前往查看源码
if ( sClosureMode == ClosureMode::Off ) {
if ( gLinkContext.verboseWarnings )
dyld::log("dyld: not using closures\n");
} else {
...
// try using launch closure
if ( mainClosure != nullptr ) {
...
bool launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
...
if ( launched ) {
gLinkContext.startedInitializingMainExecutable = true;
if (sSkipMain)
result = (uintptr_t)&fake_main;
return result;
}
}
总体来说,dyld 3把很多耗时的操作都提前处理好了,极大提升了启动速度。了解dyld对APP的启动过程有一个更全面的认识,对APP的安全防护,对APP启动速度的优化都需要对dyld有深入的理解。
这篇文章主要参考了以下几篇文章: