iOS App启动时发生了什么?

引子

准备工作, 本文涉及到的Apple 开源源码如下, 这些开源库在 https://opensource.apple.com 都能下载:

  • dyld
  • objc4-750
  • libdispatch
  • libSystem

我们在一个Demo APP的AppDelegate.m中增加+load方法并且打上断点, 并且在main()函数第一语句也打上断点.

在APP运行以后, 我们可以看到在xcode调用栈中有以下内容:

0 +[AppDelegate load]
1 call_load_methods
2 load_images
3 dyld::notifySingle(...)
4 ImageLoader::recursiveInitialization(...)
5 ImageLoader::processInitializers(...)
6 ImageLoader::runInitializers(...)
7 dyld::_main(...)
8 dyldbootstrap::start(...)
9 _dyld_start

我们发现, APP暂停在了+[AppDelegate load]而不是main()函数中, 也就是说+load方法会在main()函数之前执行. 那么为什么会出现这种情况呢.

注意使用真机调试和Simulator调试可能调用栈不太一样, 这里建议使用真机调试.

dyld的_main方法

dyld被称为动态链接器, 在dyldStartup.s文件中, 有一个_dyld_start方法, 这个方法是一个汇编方法

__dyld_start:
    popq    %rdi        # param1 = mh of app
    pushq   $0      # push a zero for debugger end of frames marker
    movq    %rsp,%rbp   # pointer to base of kernel frame
    andq    $-16,%rsp       # force SSE alignment
    subq    $16,%rsp    # room for local variables
    
    # call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
    movl    8(%rbp),%esi    # param2 = argc into %esi
    leaq    16(%rbp),%rdx   # param3 = &argv[0] into %rdx
    movq    __dyld_start_static(%rip), %r8
    leaq    __dyld_start(%rip), %rcx
    subq     %r8, %rcx  # param4 = slide into %rcx
    leaq    ___dso_handle(%rip),%r8 # param5 = dyldsMachHeader
    leaq    -8(%rbp),%r9
    call    __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
    movq    -8(%rbp),%rdi
    cmpq    $0,%rdi
    jne Lnew

会调用dyldbootstrap::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.
//
//  核心方法: dyldbootstrap::start(...), 首先bootstrapping dyld, 然后调用dyld::_main核心方法
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 dyld, 我们必须 fixeup 一下dyly中的内容.
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

    // allow dyld to use mach messaging
    mach_init();

    // 内核设置的env pointers, 也就是环境参数
    // 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

    // 当前这里 已经完成了 bootstrapping dyld过程, 后面需要调用 dyld 的_main函数
    // now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);

    // 前面经过很多设置最后调用 dyld::_main 方法
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

代码逻辑比较清晰, 经过很多配置的参数然后调用dyld::_main()方法:

注意dyld::_main(...)App中的main()是两个不同的方法.

/*
 //
 // Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
 // sets up some registers and call this function.
 //
 // Returns address of main() in target program which __dyld_start jumps to
 //
 */
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
    //1 设置运行环境,处理环境变量
    uintptr_t result = 0;
    sMainExecutableMachHeader = mainExecutableMH;

    CRSetCrashLogMessage("dyld: launch started");
    // 设置上下文
    setContext(mainExecutableMH, argc, argv, envp, apple);

    // Pickup the pointer to the exec path.
    // 获取可执行文件的路径
    sExecPath = _simple_getenv(apple, "executable_path");

    // <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
    if (!sExecPath) sExecPath = apple[0];

    // 将可执行文件的路径由相对路径转化成绝对路径
    bool ignoreEnvironmentVariables = false;
    if ( sExecPath[0] != '/' ) {
        // have relative path, use cwd to make absolute
        char cwdbuff[MAXPATHLEN];
        if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
            // maybe use static buffer to avoid calling malloc so early...
            char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
            strcpy(s, cwdbuff);
            strcat(s, "/");
            strcat(s, sExecPath);
            sExecPath = s;
        }
    }
    // Remember short name of process for later logging
    // 获取可执行文件去除前面的路径, 获取它的name,
    sExecShortName = ::strrchr(sExecPath, '/');
    if ( sExecShortName != NULL )
        ++sExecShortName;
    else
        sExecShortName = sExecPath;

    // 配置进程是否受到限制
    sProcessIsRestricted = processRestricted(mainExecutableMH, &ignoreEnvironmentVariables, &sProcessRequiresLibraryValidation);
    if ( sProcessIsRestricted ) {
#if SUPPORT_LC_DYLD_ENVIRONMENT
        checkLoadCommandEnvironmentVariables();
#endif  
        pruneEnvironmentVariables(envp, &apple);
        // set again because envp and apple may have changed or moved
        setContext(mainExecutableMH, argc, argv, envp, apple);
    } else {
        if ( !ignoreEnvironmentVariables )
            checkEnvironmentVariables(envp);// 检查环境变量
        defaultUninitializedFallbackPaths(envp);
    }
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);

    // 获取当前设备的CPU架构信息
    getHostInfo(mainExecutableMH, mainExecutableSlide);

    // install gdb notifier
    // 注册gdb的监听者, 用于调试
    stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
    stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
    // make initial allocations large enough that it is unlikely to need to be re-alloced
    sAllImages.reserve(INITIAL_IMAGE_COUNT);
    sImageRoots.reserve(16);
    sAddImageCallbacks.reserve(4);
    sRemoveImageCallbacks.reserve(4);
    sImageFilesNeedingTermination.reserve(16);
    sImageFilesNeedingDOFUnregistration.reserve(8);
    
#ifdef WAIT_FOR_SYSTEM_ORDER_HANDSHAKE
    // <rdar://problem/6849505> Add gating mechanism to dyld support system order file generation process
    WAIT_FOR_SYSTEM_ORDER_HANDSHAKE(dyld::gProcessInfo->systemOrderFlag);
#endif
    
    //2 初始化主程序
    try {
        // add dyld itself to UUID list
        addDyldImageToUUIDList();
        CRSetCrashLogMessage(sLoadingCrashMessage);
        // instantiate ImageLoader for main executable
        // 加载sExecPath路径下的可执行文件, 实例化一个ImageLoader对象.
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

        // 设置上下文, 将MainExecutable 这个 ImageLoader设置给链接上下文, 配置链接上下文其他变量
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.processIsRestricted = sProcessIsRestricted;
        gLinkContext.processRequiresLibraryValidation = sProcessRequiresLibraryValidation;
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

        // load shared cache
        //3 加载共享缓存
        checkSharedRegionDisable();
    #if DYLD_SHARED_CACHE_SUPPORT
        if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
            mapSharedCache();
    #endif

        // Now that shared cache is loaded, setup an versioned dylib overrides
    #if SUPPORT_VERSIONED_PATHS
        checkVersionedPaths();
    #endif

        // load any inserted libraries
        //4 加载插入的动态库
        //变量 `DYLD_INSERT_LIBRARIES` 环境变量, 调用`loadInsertedDylib`方法加载所有要插入的库, 这些库都被加入到`sAllImages`数组中
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }

        // record count of inserted libraries so that a flat search will look at 
        // inserted libraries, then main, then others.
        sInsertedDylibCount = sAllImages.size()-1;

        // link main executable
        //5 链接主程序
        // 开始链接主程序, 此时主程序已经被加载到gLinkContext.mainExecutable中, 调用 link 链接主程序。内核调用的是ImageLoader::link 函数。
        gLinkContext.linkingMainExecutable = true;
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
        sMainExecutable->setNeverUnloadRecursive();
        if ( sMainExecutable->forceFlat() ) {
            gLinkContext.bindFlat = true;
            gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
        }

        //6 链接插入的动态库
        //对 sAllimages (除了主程序的Image外)中的库调用link进行链接,然后调用 registerInterposing 注册符号插入, 例如是libSystem就是此时加入的
        // link any inserted libraries
        // do this after linking main executable so that any dylibs pulled in by inserted 
        // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
        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));
                image->setNeverUnloadRecursive();
            }
            // 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];
                // 注册符号插入,Interposition, 是通过编写与函数库同名的函数来取代函数库的行为.
                image->registerInterposing();
            }
        }

        // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
        for (int i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
            ImageLoader* image = sAllImages[i];
            if ( image->inSharedCache() )
                continue;
            image->registerInterposing();
        }

        // apply interposing to initial set of images
        for(int i=0; i < sImageRoots.size(); ++i) {
            sImageRoots[i]->applyInterposing(gLinkContext);
        }
        gLinkContext.linkingMainExecutable = false;

        //7 执行弱符号绑定
        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        sMainExecutable->weakBind(gLinkContext);

        CRSetCrashLogMessage("dyld: launch, running initializers");
    #if SUPPORT_OLD_CRT_INITIALIZATION
        // Old way is to run initializers via a callback from crt1.o
        if ( ! gRunInitializersOldWay ) 
            initializeMainExecutable(); 
    #else

        //8 执行初始化方法
        // 执行初始化方法, 其中`+load` 和constructor方法就是在这里执行, `initializeMainExecutable`方法先是内部调用动态库的初始化方法, 然后调用主程序的初始化方法
        // run all initializers
        initializeMainExecutable(); 
    #endif

        //9 查找APP入口点并返回
        // find entry point for main executable
        result = (uintptr_t)sMainExecutable->getThreadPC();
        if ( result != 0 ) {
            // main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
            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->getMain();
            *startGlue = 0;
        }
    }
    catch(const char* message) {
        syncAllImages();
        halt(message);
    }
    catch(...) {
        dyld::log("dyld: launch failed\n");
    }

    CRSetCrashLogMessage(NULL);
    
    return result;
}

这里dyld::_main(...方法比较长, 这里简单归纳一下:

1. 设置运行环境,处理环境变量

代码在开始时候, 将传入的变量mainExecutableMH赋值给了sMainExecutableMachHeader, 这是一个macho_header类型的变量, 其结构体内容就是本章前面介绍的mach_header结构体, 表示的是当前主程序的Mach-O头部信息, 有了头部信息, 加载器就可以从头开始, 遍历整个Mach-O文件的信息.

接着执行了setContext(), 此方法设置了全局一个链接上下文, 包括一些回调函数, 参数与标志设置信息, 设置的回调函数都是dyld本模块实现的, 如loadLibrary方法就是本模块的libraryLocator()方法, 负责加载动态库。在设置完这些信息后, 执行processRestricted()方法判断进程是否受限, 如果进程受限后,执行了以下三个方法:

  1. checkLoadCommandEnvironmentVariables():遍历Mach-O中所有的LC_DYLD_ENVIRONMENT加载命令, 然后调用processDyldEnvironmentVariable()对不同的环境变量做相应的处理。
  2. pruneEnvironmentVariables():删除进程的LD_LIBRARY_PATH与所有以DYLD_开头的环境变量, 这样以后创建的子进程就不包含这些环境变量了.
  3. setContext(): 重新设置链接上下文。这一步执行的主要目的是由于环境变量发生变化了, 需要更新进程的envp与apple参数。
2. 初始化主程序

这一步主要执行instantiateFromLoadedImage(), 它的代码如下:
isCompatibleMachO()判断程序与当前的系统是否兼容, 如果兼容接下来就调用instantiateMainExecutable()实例化主程序, 接着调用addImage(), 会将主程序添加到全局主列表sAllImages中, 最后调用addMappedRange()申请内存, 更新主程序映像映射的内存区,做完这些工作,第二步初始化主程序就算完成了.

3. 加载共享缓存

这一步主要执行mapSharedCache()来映射共享缓存, 该函数先通过_shared_region_check_np()来检查缓存是否已经映射到了共享区域了, 如果已经映射了, 就更新缓存的slideUUID, 然后返回.

反之, 判断系统是否处于安全启动模式(safe-boot mode)下,如果是就删除缓存文件并返回, 正常启动的情况下, 接下来调用openSharedCacheFile()打开缓存文件, 该函数在sSharedCacheDir路径下, 打开与系统当前cpu架构匹配的缓存文件,也就是/var/db/dyld/dyld_shared_cache_x86_64h, 接着读取缓存文件的前8192字节, 解析缓存头dyld_cache_header的信息, 将解析好的缓存信息存入mappings变量, 最后调用_shared_region_map_and_slide_np()完成真正的映射工作.

共享缓存加载完毕后, 接着进行动态库的版本化重载,这主要通过函数checkVersionedPaths()完成, 该函数读取DYLD_VERSIONED_LIBRARY_PATHDYLD_VERSIONED_FRAMEWORK_PATH环境变量, 将指定版本的库比当前加载的库的版本做比较, 如果当前的库版本更高的话, 就使用新版本的库来替换掉旧版本的。

4 加载插入的动态库

这一步循环遍历DYLD_INSERT_LIBRARIES环境变量中指定的动态库列表, 并调用loadInsertedDylib()将其加载, 该函数调用load()完成加载工作. load()会调用loadPhase0()尝试从文件加载,loadPhase0()会向下调用下一层phase来查找动态库的路径, 直到loadPhase6(),查找的顺序为DYLD_ROOT_PATH->LD_LIBRARY_PATH->DYLD_FRAMEWORK_PATH->原始路径->DYLD_FALLBACK_LIBRARY_PATH,找到后调用ImageLoaderMachO::instantiateFromFile()来实例化一个ImageLoader, 之后调用checkandAddImage()验证映像并将其加入到全局映像列表中. 如果loadPhase0()返回为空, 表示在路径中没有找到动态库, 就尝试从共享缓存中查找, 找到就调用ImageLoaderMachO::instantiateFromCache()从缓存中加载, 否则就抛出没找到映像的异常.

5 链接主程序

这一步执行link()完成主程序的链接操作, 该函数调用了ImageLoader自身的link()函数, 主要的目的是将实例化的主程序的动态数据进行修正, 达到让进程可用的目的, 典型的就是主程序中的符号表修正操作.

6 链接插入的动态库

链接插入的动态库与链接主程序一样, 都是使用的link(), 插入的动态库列表是前面调用addImage()保存到sAllImages中的, 之后, 循环获取每一个动态库的ImageLoader, 调用link()对其进行链接, 注意: sAllImages中保存的第一项是主程序的映像. 接下来调用每个映像的registerInterposing()方法来注册动态库插入与调用applyInterposing()应用插入操作.

registerInterposing()查找__DATA段__interpose节区, 找到需要应用插入操作(也可以叫作符号地址替换)的数据, 然后做一些检查后, 将要替换的符号与被替换的符号信息存入fgInterposingTuples列表中, 供以后具体符号替换时查询. applyInterposing()调用了虚方法doInterpose()来做符号替换操作, 在ImageLoaderMachOCompressed中实际是调用了eachBind()eachLazyBind()分别对常规的符号与延迟加载的符号进行应用插入操作, 具体使用的是interposeAt(), 该方法调用interposedAddress()fgInterposingTuples中查找要替换的符号地址, 找到后然后进行最终的符号地址替换.

注意是libSystem就是此时加入的, runtime的初始化函数_objc_init()就是libSystem调用的

7 执行弱符号绑定

weakBind()函数执行弱符号绑定. 首先通过调用context的getCoalescedImages()sAllImages中所有含有弱符号的映像合并成一个列表,合并完后调用initializeCoalIterator()对映像进行排序,排序完成后调用incrementCoalIterator()收集需要进行绑定的弱符号,后者是一个虚函数,在ImageLoaderMachOCompressed中,该函数读取映像动态链接信息的weak_bind_offweak_bind_size来确定弱符号的数据偏移与大小,然后挨个计算它们的地址信息. 之后调用getAddressCoalIterator(),按照映像的加载顺序在导出表中查找符号的地址,找到后调用updateUsesCoalIterator()执行最终的绑定操作,执行绑定的是bindLocation(),前面有讲过,此处不再赘述.

8 执行初始化方法

执行初始化的方法是:

  1. initializeMainExecutable()
  2. ImageLoader::runInitializers()
  3. ImageLoader::processInitializers()
  4. ImageLoader::recursiveInitialization()
  5. 先调用image自己的初始化函数initializer, 然后发出通知, 告知观察者当前image已经完成加载, 状态是dyld_image_state_initialized
    • doInitialization
      • doImageInit, 获取mach-o的init方法的地址并调用
      • doModInitFunctions, 获取mach-o的static initializer的地址并调用
    • context.notifySingle(dyld_image_state_initialized, this), 对外发出通知,当前image已经初始化完毕.
9 APP的main函数地址入口点并返回

这一步调用主程序映像的getThreadPC()函数来查找主程序的LC_MAIN加载命令获取程序的入口点, 没找到就调用getMain()LC_UNIXTHREAD加载命令中去找, 找到后就跳到入口点指定的地址, dyld::main函数会返回程序的main函数地址, main函数被调用, 从而代码来到了我们熟悉的程序入口。

到这里, dyld整个加载动态库的过程就算完成了。

关于initializeMainExecutable方法中, image的初始化详解

前面一节中第8步的内容, 是针对所有image执行initializeMainExecutable方法, 具体的代码如下:

/**
 initializeMainExecutable 执行初始化方法,其中 +load 和 constructor 方法就是在这里执行。
 initializeMainExecutable 内部先调用了动态库的初始化方法,后调用主程序的初始化方法。
 */
void initializeMainExecutable() {
    // record that we've reached this step
    // 进行标志 gLinkContext, 已经开始 initialize MainExecutable这个image.
    gLinkContext.startedInitializingMainExecutable = true;

    // run initialzers for any inserted dylibs
    // 给被插入的所有的 dylibs 进行初始化 -- 调用 initialzers
    ImageLoader::InitializerTimingList initializerTimes[sAllImages.size()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        // 从 sImageRoots 中的第一个变量是 MainExcutable image, 因此这里初始化的时候需要跳过第一个数据, 对其他后面插入的dylib进行调用ImageLoader::runInitializers进行初始化
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }

    // 单独对 main executable调用ImageLoader::runInitializers进行初始化
    // run initializers for main executable and everything it brings up 
    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 )
        ImageLoaderMachO::printStatistics((unsigned int)sAllImages.size(), initializerTimes[0]);
}

其中, 主要是调用所有imageImageLoader::runInitializers()方法, 代码如下:

/**
 初始化过程如下, 实际调用ImageLoader::processInitializers
 @param context
 @param timingInfo
 */
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.images[0] = this;

    // 初始化当前 imageLoader 中的 image镜像的实际调用方法 ImageLoader::processInitializers
    processInitializers(context, thisThread, timingInfo, up);

    context.notifyBatch(dyld_image_state_initialized);
    mach_port_deallocate(mach_task_self(), thisThread);
    uint64_t t2 = mach_absolute_time();
    fgTotalInitTime += (t2 - t1);
}

其中最核心的方法是ImageLoader::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.
// 调用 recursiveInitialization 方法!!!!
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread, InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images) {
    uint32_t maxImageCount = context.imageCount();
    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.
    // 处理当前image依赖 dylib动态库, 调用 recursiveInitialization 方法!!!
    for (uintptr_t i=0; i < images.count; ++i) {
        images.images[i]->recursiveInitialization(context, thisThread, timingInfo, ups);
    }
    // If any upward dependencies remain, init them.
    if ( ups.count > 0 )
        processInitializers(context, thisThread, timingInfo, ups);
}

ImageLoader::recursiveInitialization方法中, 会递归的调用当前image的依赖的dylib动态库的初始化函数进行初始化, 然后才调用doInitialization来调用自己的初始化函数, 在中间image的state状态切换时,对外通过notifySingle方法给外部环境contenxt发出状态变化的通知(外部如果有内容监听了相关通知, 那么会执行相应回调, runtime就是如此):

/**
 递归调用 image 进行初始化, 先调用image依赖的image进行初始化. 直到自己
 */
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    recursive_lock lock_info(this_thread);
    recursiveSpinLock(lock_info);

    // 当前ImageLoader依赖的Image还没有初始化完, 进入if中, 如果执行完成, 直接返回
    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        uint8_t oldState = fState;
        // break cycles -> 这是设置当前imageLoader的state接近依赖初始化.
        fState = dyld_image_state_dependents_initialized-1;
        try {
            // 首先初始化image底层的依赖库
            // 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.images[uninitUps.count] = dependentImage;
                        uninitUps.count++;
                    }
                    else if ( dependentImage->fDepth >= fDepth ) {
                        dependentImage->recursiveInitialization(context, this_thread, timingInfo, uninitUps);
                    }
                }
            }

            // record termination order
            if ( this->needsTermination() )
                context.terminationRecorder(this);

            // 到这里image底层的依赖库都递归调用, 初始化完成.

            // let objc know we are about to initialize this image
            uint64_t t1 = mach_absolute_time();
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            // 通知 runtime, 当前状态发生变化 -- image的依赖已经完全加载. 注意这里可能在runtime中注册了状态监听, 注册了callback函数, 当状态发送变化时, 会触发回调函数.
            context.notifySingle(dyld_image_state_dependents_initialized, this);

            // 初始化当前image, `ImageLoaderMachO::doInitialization`方法内部会调用image的"Initializer", 这是一个函数指针, 实际是image的初始化方法. 例如 `libSystem.dylib`, 它的初始化方法就比较特殊, 我们可以参考libSystem的init.c源码, 内部的`libsystem_initializer`函数就是初始化真正调用的函数

            //  typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[], const ProgramVars* vars);

            // _init_objc方法!!!!
            // initialize this image
            bool hasInitializers = this->doInitialization(context);

            // let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            // 通知runtime, 档期那状态发送变化 -- image自己已经完成初始化!!!!
            context.notifySingle(dyld_image_state_initialized, this);
            
            if ( hasInitializers ) {
                uint64_t t2 = mach_absolute_time();
                timingInfo.images[timingInfo.count].image = this;
                timingInfo.images[timingInfo.count].initTime = (t2-t1);
                timingInfo.count++;
            }
        }
        catch (const char* msg) {
            // this image is not initialized
            fState = oldState;
            recursiveSpinUnLock();
            throw;
        }
    }
    
    recursiveSpinUnLock();
}

我们先来看,doInitialization方法, 它会调用doImageInitdoModInitFunctions方法, 这两个方法做的工作就是从image镜像中获取这个镜像的真正的入口初始化方法initializer并调用.

注意, initalizer并非名为initalizer的方法, 而是C++静态对象初始化构造器, atribute((constructor) 进行修饰的方法, 在ImageLoader类中initializer函数指针所指向该初始化方法的地址

/**
 image 真正的初始化调用方法, 内部会调用`doImageInit`, 这个方法会调用 mach-o 的initiaizer方法. 以及 static initializers 方法.
 */
bool ImageLoaderMachO::doInitialization(const LinkContext& context) {
    CRSetCrashLogMessage2(this->getPath());

    // mach-o has -init and static initializers
    doImageInit(context); // mach-o 的init方法
    doModInitFunctions(context); // mach-o的 static initializer方法
    
    CRSetCrashLogMessage2(NULL);
    return (fHasDashInit || fHasInitializers);
}


简单看一下Initializer函数指针的的定义和调用的过程, 首先Initializer是一个函数指针, 在doModInitFunctions方法中是获取image中Initializer方法地址, 然后直接执行.

typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[], const ProgramVars* vars);

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context){
    ...
    Initializer* inits = (Initializer*)(sect->addr + fSlide);
    ...
    Initializer func = inits[i];
    ...
    func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
    ...
}

在最后会调用notifySingle通知方法, 会触发外部监听dyld_image_state_initialized状态的callback回调方法.

libdiaptch的初始化和runtime的初始化

上一节中, 我们知道一个image动态库要被初始化并且加载完成, 需要先递归加载它依赖的image动态库.

那么一个最简单的iOS程序依赖哪些动态库呢?

我们可以在一个iOS APP中设置环境变量DYLD_PRINT_INITIALIZERS为1, 然后编译运行, 控制台就会打印出当前APP依赖的所有的动态库.

具体步骤是: Edit-Scheme->Arguments->Enviroments Vairables增加一个参数 --- Name: DYLD_PRINT_INITIALIZERS, Value: 1, 然后编译, 控制台会打印如下内容:

dyld: calling initializer function 0x1805efa7c in /usr/lib/libSystem.B.dylib
dyld: calling -init function 0x1022b8bbc in /Developer/usr/lib/libBacktraceRecording.dylib
dyld: calling initializer function 0x180608098 in /usr/lib/libc++.1.dylib
dyld: calling -init function 0x181427b48 in /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
dyld: calling initializer function 0x18148737c in /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
...

我们看到第一个initializer functionlibSystem.B.dylib.

我们在Apple官网找到libSystem.dylib开源库(可能和实际有所不同), 我们看一下它的初始化方法具体做了什么工作, 我们找到libSystem源码中的init.c文件:

// libsyscall_initializer() initializes all of libSystem.dylib
// libsyscall_initializer() 就是 libSystem.dylib的initializer初始化方法
// <rdar://problem/4892197>
__attribute__((constructor))
static void
libSystem_initializer(int argc,
              const char* argv[],
              const char* envp[],
              const char* apple[],
              const struct ProgramVars* vars)
{
    ...

    // 内核初始化
    __libkernel_init(&libkernel_funcs, envp, apple, vars);
    // 平台信息初始化
    __libplatform_init(NULL, envp, apple, vars);
    // 线程初始化
    __pthread_init(&libpthread_funcs, envp, apple, vars);

    _libc_initializer(&libc_funcs, envp, apple, vars);

    // TODO: Move __malloc_init before __libc_init after breaking malloc's upward link to Libc
    __malloc_init(apple);

#if !TARGET_OS_SIMULATOR && !TARGET_OS_TV && !TARGET_OS_WATCH
    /* <rdar://problem/9664631> */
    __keymgr_initializer();
#endif

    _dyld_initializer();

    // !!!!!! GCD的初始化方法!!!!!
    //libdispatch_init里调用了到了runtime初始化方法_objc_init.我们可以、在程序中打个符号断点来验证:
    libdispatch_init();
    _libxpc_initializer();

    // must be initialized after dispatch
    _libtrace_init();
    
    ...
}

libdispatch_init里调用了到了runtime初始化方法_objc_init.

我们可以、在程序中打个符号断点来验证, 在Xcode设置Symbolic Breakpoint, 然后设置Symblic = _objc_init:

Apple官网开源了libsystem的源码
https://opensource.apple.com/tarballs/Libsystem/

libSystem_init.jpg

从图中我们看到, 先libSystem_initializer调用libdispatch_init再到_objc_init初始化runtime

runtime初始化后不会闲着, 在_objc_init中注册了几个通知, 从dyld这里接手了几个活, 其中包括负责初始化相应依赖库里的类结构, 调用依赖库里所有的+load方法:

// Initializer called by libSystem
OBJC_EXPORT void _objc_init(void);

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time

 Q:load 方法是如何被调用的?

 A:_objc_init(OBJC runtime)是 libSystem 在library初始化之前调用, 进行runtime的初始化!!!! 当 Objective-C runtime初始化的时候, 会通过 dyld_register_image_state_change_handler注册针对一些事件的监听, 在每次有新的镜像加入运行时的时候,进行回调调用`load_images`(也有其他事件触发 map_images和 unmap_image方法).

 执行 load_images 将所有包含 load 方法的文件加入列表 loadable_classes ,然后从这个列表中找到对应的 load 方法的实现,调用 load 方法。
**********************************************************************/
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();
    lock_init();
    exception_init();

    // 这里是在dyld中加入一个状态监听器, 一旦dyld监听到有新的镜像image加载完成, 就调用 load_images 方法, 并传入最新镜像的信息类别 infoList
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

前面说到image在被dyld加载的时候, notifySingle 会通知image的状态变化, 这样_objc_init中最后通过_dyld_objc_notify_register方法注册的监听器, 后面一旦有新的image被加载到dyld时, 就会调用objc::load_images方法!!!

关于runtime中的load_images, 其他文章会进行详解解答后面的内容.

启动流程总结

一个 iOS 程序会依赖于某些动态库, 可以通过otool -v -L debug-objc命令查看, 在启动的时候需要由动态链接器进行动态链接,而动态链接器自身也是动态库,因此动态链接器会首先自举自身,然后再将程序依赖的动态库依次加载进内存进行链接,动态链接做完之后,把控制权交给程序的main函数。

通过 otool 命令可以查看到依赖列表中有 /usr/lib/libSystem.B.dylib/usr/lib/libobjc.A.dylib 这样两条记录.libSystem.B.dylib 在编译的时候依赖于很多系统级别的 lib, 如 libdispatch,libsystem_c, libcommonCrypto等等.而这个libobjc.A.dylib就是objc-runtime, 其初始化函数是 _objc_init, 前面说过, 可以根据_objc_init设置symbol breakpoint进行后续的调试.

  1. 首先系统内核建立进程, 创建虚拟空间, 读取 Mach-O 文件头,并建立虚拟空间与Mach-O文件的映射关系, 然后找到 dyld 动态链接器入口 _dyld_start 之后, 控制权交给动态链接器, 接下来就是 dyld 的自举, 即 dyldbootstrap::start().
  2. 自举过程完成, 即在 dyldbootstrap::start() 函数的最末尾会调用 dyld::_main 函数, 并把此函数的返回值返还给 _dyld_start, 这个返回值是 iOS 可执行程序main函数的地址,_dyld_start 会跳到该地址继续执行,进入程序的主逻辑.
  3. 不过在这个地址返回之前, 也就是在dyld::_main函数中需要做两个特别重要的工作:
    • 重定位和初始化, 也就是动态链接的过程. 在完成了自举之后, 链接器需要装载动态链接库, 然后完成符号的重定位.
    • 如果动态链接库文件中还有相应的初始化信息,即含有 __mod_init_func section,就再需要调用其相应的初始化函数.
  4. dyld::initializeMainExecutable函数中, 程序所依赖的动态库进行各自的初始化, 因此ImageLoader::runInitializers函数被调用, 这个函数调用ImageLoader::processInitializers来处理初始化, 而某个动态库可能依赖于其它的动态库, 那么它所依赖的动态库就需要先初始化, 这里形成一个递归, 也就是ImageLoader::recursiveInitialization
  5. 动态库初始化函数的真正调用是在 ImageLoaderMachO::doModInitFunctions函数中, 对于 libSystem.B.dylib来说其初始化函数是 libSystem_initializer, 在这个函数中libdispatch_init被调用, libSystem 以及 libdispatch 也是开源的, 可以查看相关源码.
  6. libdispatch_init中, 对 Runtime_objc_init进行了调用, 而在 Runtime 的初始化过程中, 查看源码可以看到 Runtimedyld 绑定了回调, 当 image 加载到内存后, dyld 会通知 Runtime 进行处理, Runtime 接手后调用 map_images 做解析和处理, 把Category 的实例方法, 协议以及属性添加到类上, 把 Category 的类方法和协议添加到类的 metaclass 上; 接下来 load_images 中调用 call_load_methods 方法, 遍历所有加载进来的 Class, 按继承层级依次调用 Classload 方法和其 Categoryload 方法(这部分内容,会在后面针对runtime的文章进行分析)
  7. 在所有的动态库做好符号重定位和初始化工作之后, 也就是 dyld::_main 临近末尾的时候,dyld 会获取 main 函数的地址返回给 dyld, dyld 紧接着调用 main 函数, 将控制权交换给主程序, 程序开始真正的执行.

结语

最后一句话概括APP的启动:

内核exec做好准备工作,转移控制权给dyld->dyld加载依赖库->libdispatch初始化->runtime初始化->main开始执行

APP的启动优化

可以参考: https://www.jianshu.com/p/7096478ccbe7

参考文章

https://www.jianshu.com/p/7096478ccbe7
https://www.jianshu.com/p/43db6b0aab8e
https://blog.csdn.net/fishmai/article/details/51419824
https://blog.csdn.net/guojin08/article/details/70308576
https://blog.csdn.net/nathan1987_/article/details/78591468
https://www.jianshu.com/p/885c8077b27d
https://www.cnblogs.com/maizi008/p/5086103.html
https://blog.csdn.net/TuGeLe/article/details/81609604
https://www.jianshu.com/p/5f337da8fbef
https://www.jianshu.com/p/231b1cebf477
https://www.cnblogs.com/xs514521/p/7010458.html
https://www.jianshu.com/p/73a99303cd91

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容