dyld

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

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

从调用堆栈中咱们能够看到,程序由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…markdown

从调用堆栈跟踪

咱们先跟着刚刚的调用堆栈来跟踪一遍,首先打开咱们刚刚下载的dyld源码,搜索咱们刚刚在调用堆栈里看到的最外层的调用_dyld_startsession

_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.pngapp

initializeMainExecutable

这个initializeMainExecutable函数在_main里面调用了两次,不过是分不一样架构的,也是能够直接command加左键定位到函数实现的,从这个函数里的注释中能够看到,是先执行的全部插入的库的initialzers方法,再执行咱们主程序的initialzers方法的,这也说明了咱们写的Framework中的load方法会比咱们主程序的load方法先执行;若是不认为run initialzers就是调用load方法,能够跟着调用堆栈流程走完,你就会知道究竟是不是了 image.pngide

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有深刻的理解。

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

相关文章
相关标签/搜索