dyld

1,316 阅读20分钟

+load和main()谁先调用?有经验的iOSer们会毫不犹豫的回答出来是load方法,但为什么是load方法呢?今天我们来探讨一下底层的原理

新建一个项目,在AppDelegate里添加load方法,打上一个断点就会看到如下图所示的调用堆栈,如果嫌左侧太长了看不全,也可以在控制台输入bt指令查看调用堆栈 image.png

从调用堆栈中我们可以看到,程序由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

image.png 从截图中可以看到,搜索结果的第一部分和第二部分,都不可能是我们想要找的东西,第一部分.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(很快就找到了 image.png 函数的return的前面一行有一个appsSlide变量,这个变量是ASLR地址空间配置随机加载技术的应用,这是是一种防范内存损坏漏洞被利用的计算机安全技术
这个start函数里面的最后一行代码调用了_main()函数,这个比较舒服,不需要我们再去找了,直接按住command然后左键点击就可以跳转到对应的实现代码了

_main

这个_main函数就是启动我们APP的关键代码,从它的行数就可以看出来它的分量了,从6455行到7303一共848行(这里就不贴它的全部源码了)好家伙,还是头一回看到一个C函数写这么长的,本来是想吐槽一下的,但一想到这是苹果的工程师写的底层代码,咱还是老老实实看源码...这部分的代码就是今天重点中的重点,我们在下一部分重点讲这个函数,现在还是接着调用堆栈继续往后面走,后面的几个都挺好找的 image.png

initializeMainExecutable

这个initializeMainExecutable函数在_main里面调用了两次,不过是分不同架构的,也是可以直接command加左键定位到函数实现的,从这个函数里的注释中可以看到,是先执行的所有插入的库的initialzers方法,再执行我们主程序的initialzers方法的,这也说明了我们写的Framework中的load方法会比我们主程序的load方法先执行;如果不认为run initialzers就是调用load方法,可以跟着调用堆栈流程走完,你就会知道到底是不是了 image.png

runInitializers

这个runInitializers函数也是在上面initializeMainExecutable函数里面调用了两次,从两次调用的注释来看,上面调用的是所有插入的动态库的初始化方法,而下面的才是我们主程序调用初始化方法,所以在[AppDelegate laod]方法中打的断点卡住的应该是下面的这段代码;这个函数也比较好找,直接搜runInitializers(只有12个结果在5个文件里,载结合它前面的ImageLoader作用域,函数的参数,很快就能找到它的实现 image.png

processInitializers

结合调用堆栈,找到runInitializers里面的processInitializers函数实现很容易,找到processInitializers函数的实现也很简单,可以直接command加左键点击就到了 image.png

recursiveInitialization

同样是在ImageLoader.cpp文件内,processInitializers就能直接command加左键定位到实现代码,而recursiveInitialization却不可以,不知道为什么,不过也没什么大问题,recursiveInitialization在当前文件一搜就找到实现了 image.png 这里有个小细节可以说一下,在第一次content.notifySingle()之后有一个doInitialization函数,这个函数 里面又会有两个初始化函数 image.png doInitialization()内部首先调用doImageInit来执行镜像的初始化函数,也就是LC_ROUTINES_COMMAND中记录的函数。 再执行doModInitFunctions()方法来解析并执行_DATA_,__mod_init_func这个section中保存的函数。使用__attribute__((constructor))开头的C函数会保存在这里面,如图所示: image.png

notifySingle

notifySingle的调用同样是在上一个函数recursiveInitialization的实现里面,但是notifySingle的实现结合调用堆栈它前面的作用域来看,不在ImageLoader里面,那就直接全局搜索notifySingle(,也比较容易找 image.png 接下来,由notifySingleload_images会发现调用的地方已经不是在dyld了...那么如何实现代码的执行从一个程序跳到另一个程序呢?有很多种办法,通知,代理,block,函数作为参数传递,我们仔细观察一下notifySingle里面有没有以上任何一种,会发现下面这里有一个不太一样的地方sNotifyObjCInit image.png 那接下来就看看这个sNotifyObjCInit变量是在哪里被赋值的,搜索一番后发现在这里被赋值了 image.png 紧接着搜一搜这个registerObjCNotifiers在哪里被调用了 image.png 搜索一番后发现这里是dyld提供的对外部的接口...那就说明我们在dyld里面应该是找不到这个调用的地方了,不过至少我们找到了这个对外的接口函数_dyld_objc_notify_register,这个时候,我们可以回到最开始新建的项目中去,下一个_dyld_objc_notify_register的符号断点 image.png 会发现是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目录,是不是可以看到一个非常眼熟的东西 image.png
这样看来我们真的找对地方了,那就直接全局搜索我们刚刚获取到的_dyld_objc_notify_register函数,看看是不是_objc_init里面调用了它 image.png 果然如此,看到这里,就会发现dyld里面的sNotifyObjCInit变量是被赋值了一个load_images的函数,到这里就完全能解释的通,从调用堆栈的第2帧到第1帧的执行了,这个load_images可以直接按住command点击定位到实现代码

load_images

接下来就是看怎么从libobjc.A.dylib的load_images函数到我们的断点[AppDelegate load]方法的了 image.png 很明显,我们需要查看call_load_methods函数 image.png 在这个函数里,我们就可以明显的看到先是调用所有类Class的load方法,然后再调用所有分类category中的load方法,查看call_class_loads的实现 image.png 到这里,我们断点卡住的所有调用堆栈就全部跟踪完毕了

需要了解的是,这个时候我们依然还在dyld的_main函数里面,连_main里面的initializeMainExecutable都没有执行完...而我们主程序的main()是在什么时候调用的呢?dyld的_main函数的返回值result就是我们主程序的入口,_main执行完毕之后,会把返回值返回到start函数,start函数又会把返回值返回到我们的最初的入口_dyld_start里面的那条bl指令后,x0就是返回的我们主程序入口,接下来一个mov x16,x0看注释也知道是将主程序入口地址保存到x16了,再搜索一下x16发现后面基本都是各种情况下的br或者braazx16,就是跳转到我们的主程序了,所以我们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为例,这个应该可以作为优化启动时间的参考数据 image.png image.png

3.加载共享缓存

这里先说明一下,iOS的共享缓存机制:在iOS系统中,每个程序依赖的动态库都需要通过dyld一个个加载到内存,然而,很多系统库基本上是每个程序都会用到的(比如UIKit,Foundation...),如果每个程序启动运行的时候都重复的去加载一次,势必会造成运行缓慢,不必要的内存消耗,为了优化启动速度和节约内存消耗,共享缓存机制就出现了;这里还要说一句,在iOS中,只有系统库才能称为真正意义上的动态库,我们普通开发者开发的各种格式的库,都只能是静态库;所有默认的动态链接库被合并成一个大的缓存文件,放在/System/Library/Caches/com.apple.dyld/目录下,按不同的架构分别保存,想要分析某个系统库,可以从dyld_shared_cache里将原始的二进制文件提取出来;感兴趣的可以根据我的第一篇参考文章上的步骤去尝试一下

这一步先调用checkSharedRegionDisable()检查共享缓存是否禁用。该函数的iOS实现部分仅有一句注释,从注释我们可以推断iOS必须开启共享缓存才能正常工作,代码如下: image.png 接下来调用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有深入的理解。

这篇文章主要参考了以下几篇文章: