iOS App啟動(dòng)時(shí)發(fā)生了什么?

引子

準(zhǔn)備工作, 本文涉及到的Apple 開源源碼如下, 這些開源庫在 https://opensource.apple.com 都能下載:

  • dyld
  • objc4-750
  • libdispatch
  • libSystem

我們在一個(gè)Demo APP的AppDelegate.m中增加+load方法并且打上斷點(diǎn), 并且在main()函數(shù)第一語句也打上斷點(diǎn).

在APP運(yùn)行以后, 我們可以看到在xcode調(diào)用棧中有以下內(nèi)容:

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

我們發(fā)現(xiàn), APP暫停在了+[AppDelegate load]而不是main()函數(shù)中, 也就是說+load方法會(huì)在main()函數(shù)之前執(zhí)行. 那么為什么會(huì)出現(xiàn)這種情況呢.

注意使用真機(jī)調(diào)試和Simulator調(diào)試可能調(diào)用棧不太一樣, 這里建議使用真機(jī)調(diào)試.

dyld的_main方法

dyld被稱為動(dòng)態(tài)鏈接器, 在dyldStartup.s文件中, 有一個(gè)_dyld_start方法, 這個(gè)方法是一個(gè)匯編方法

__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

會(huì)調(diào)用dyldbootstrap::start(...)方法, 這個(gè)方法:

//  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, 然后調(diào)用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中的內(nèi)容.
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

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

    // 內(nèi)核設(shè)置的env pointers, 也就是環(huán)境參數(shù)
    // 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

    // 當(dāng)前這里 已經(jīng)完成了 bootstrapping dyld過程, 后面需要調(diào)用 dyld 的_main函數(shù)
    // now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);

    // 前面經(jīng)過很多設(shè)置最后調(diào)用 dyld::_main 方法
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

代碼邏輯比較清晰, 經(jīng)過很多配置的參數(shù)然后調(diào)用dyld::_main()方法:

注意dyld::_main(...)App中的main()是兩個(gè)不同的方法.

/*
 //
 // 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 設(shè)置運(yùn)行環(huán)境,處理環(huán)境變量
    uintptr_t result = 0;
    sMainExecutableMachHeader = mainExecutableMH;

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

    // Pickup the pointer to the exec path.
    // 獲取可執(zhí)行文件的路徑
    sExecPath = _simple_getenv(apple, "executable_path");

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

    // 將可執(zhí)行文件的路徑由相對路徑轉(zhuǎn)化成絕對路徑
    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
    // 獲取可執(zhí)行文件去除前面的路徑, 獲取它的name,
    sExecShortName = ::strrchr(sExecPath, '/');
    if ( sExecShortName != NULL )
        ++sExecShortName;
    else
        sExecShortName = sExecPath;

    // 配置進(jìn)程是否受到限制
    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);// 檢查環(huán)境變量
        defaultUninitializedFallbackPaths(envp);
    }
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);

    // 獲取當(dāng)前設(shè)備的CPU架構(gòu)信息
    getHostInfo(mainExecutableMH, mainExecutableSlide);

    // install gdb notifier
    // 注冊gdb的監(jiān)聽者, 用于調(diào)試
    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路徑下的可執(zhí)行文件, 實(shí)例化一個(gè)ImageLoader對象.
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

        // 設(shè)置上下文, 將MainExecutable 這個(gè) ImageLoader設(shè)置給鏈接上下文, 配置鏈接上下文其他變量
        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 加載插入的動(dòng)態(tài)庫
        //變量 `DYLD_INSERT_LIBRARIES` 環(huán)境變量, 調(diào)用`loadInsertedDylib`方法加載所有要插入的庫, 這些庫都被加入到`sAllImages`數(shù)組中
        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 鏈接主程序
        // 開始鏈接主程序, 此時(shí)主程序已經(jīng)被加載到gLinkContext.mainExecutable中, 調(diào)用 link 鏈接主程序。內(nèi)核調(diào)用的是ImageLoader::link 函數(shù)。
        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 鏈接插入的動(dòng)態(tài)庫
        //對 sAllimages (除了主程序的Image外)中的庫調(diào)用link進(jìn)行鏈接,然后調(diào)用 registerInterposing 注冊符號插入, 例如是libSystem就是此時(shí)加入的
        // 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, 是通過編寫與函數(shù)庫同名的函數(shù)來取代函數(shù)庫的行為.
                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 執(zhí)行弱符號綁定
        // <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 執(zhí)行初始化方法
        // 執(zhí)行初始化方法, 其中`+load` 和constructor方法就是在這里執(zhí)行, `initializeMainExecutable`方法先是內(nèi)部調(diào)用動(dòng)態(tài)庫的初始化方法, 然后調(diào)用主程序的初始化方法
        // run all initializers
        initializeMainExecutable(); 
    #endif

        //9 查找APP入口點(diǎn)并返回
        // 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. 設(shè)置運(yùn)行環(huán)境,處理環(huán)境變量

代碼在開始時(shí)候, 將傳入的變量mainExecutableMH賦值給了sMainExecutableMachHeader, 這是一個(gè)macho_header類型的變量, 其結(jié)構(gòu)體內(nèi)容就是本章前面介紹的mach_header結(jié)構(gòu)體, 表示的是當(dāng)前主程序的Mach-O頭部信息, 有了頭部信息, 加載器就可以從頭開始, 遍歷整個(gè)Mach-O文件的信息.

接著執(zhí)行了setContext(), 此方法設(shè)置了全局一個(gè)鏈接上下文, 包括一些回調(diào)函數(shù), 參數(shù)與標(biāo)志設(shè)置信息, 設(shè)置的回調(diào)函數(shù)都是dyld本模塊實(shí)現(xiàn)的, 如loadLibrary方法就是本模塊的libraryLocator()方法, 負(fù)責(zé)加載動(dòng)態(tài)庫。在設(shè)置完這些信息后, 執(zhí)行processRestricted()方法判斷進(jìn)程是否受限, 如果進(jìn)程受限后,執(zhí)行了以下三個(gè)方法:

  1. checkLoadCommandEnvironmentVariables():遍歷Mach-O中所有的LC_DYLD_ENVIRONMENT加載命令, 然后調(diào)用processDyldEnvironmentVariable()對不同的環(huán)境變量做相應(yīng)的處理。
  2. pruneEnvironmentVariables():刪除進(jìn)程的LD_LIBRARY_PATH與所有以DYLD_開頭的環(huán)境變量, 這樣以后創(chuàng)建的子進(jìn)程就不包含這些環(huán)境變量了.
  3. setContext(): 重新設(shè)置鏈接上下文。這一步執(zhí)行的主要目的是由于環(huán)境變量發(fā)生變化了, 需要更新進(jìn)程的envp與apple參數(shù)。
2. 初始化主程序

這一步主要執(zhí)行instantiateFromLoadedImage(), 它的代碼如下:
isCompatibleMachO()判斷程序與當(dāng)前的系統(tǒng)是否兼容, 如果兼容接下來就調(diào)用instantiateMainExecutable()實(shí)例化主程序, 接著調(diào)用addImage(), 會(huì)將主程序添加到全局主列表sAllImages中, 最后調(diào)用addMappedRange()申請內(nèi)存, 更新主程序映像映射的內(nèi)存區(qū),做完這些工作,第二步初始化主程序就算完成了.

3. 加載共享緩存

這一步主要執(zhí)行mapSharedCache()來映射共享緩存, 該函數(shù)先通過_shared_region_check_np()來檢查緩存是否已經(jīng)映射到了共享區(qū)域了, 如果已經(jīng)映射了, 就更新緩存的slideUUID, 然后返回.

反之, 判斷系統(tǒng)是否處于安全啟動(dòng)模式(safe-boot mode)下,如果是就刪除緩存文件并返回, 正常啟動(dòng)的情況下, 接下來調(diào)用openSharedCacheFile()打開緩存文件, 該函數(shù)在sSharedCacheDir路徑下, 打開與系統(tǒng)當(dāng)前cpu架構(gòu)匹配的緩存文件,也就是/var/db/dyld/dyld_shared_cache_x86_64h, 接著讀取緩存文件的前8192字節(jié), 解析緩存頭dyld_cache_header的信息, 將解析好的緩存信息存入mappings變量, 最后調(diào)用_shared_region_map_and_slide_np()完成真正的映射工作.

共享緩存加載完畢后, 接著進(jìn)行動(dòng)態(tài)庫的版本化重載,這主要通過函數(shù)checkVersionedPaths()完成, 該函數(shù)讀取DYLD_VERSIONED_LIBRARY_PATHDYLD_VERSIONED_FRAMEWORK_PATH環(huán)境變量, 將指定版本的庫比當(dāng)前加載的庫的版本做比較, 如果當(dāng)前的庫版本更高的話, 就使用新版本的庫來替換掉舊版本的。

4 加載插入的動(dòng)態(tài)庫

這一步循環(huán)遍歷DYLD_INSERT_LIBRARIES環(huán)境變量中指定的動(dòng)態(tài)庫列表, 并調(diào)用loadInsertedDylib()將其加載, 該函數(shù)調(diào)用load()完成加載工作. load()會(huì)調(diào)用loadPhase0()嘗試從文件加載,loadPhase0()會(huì)向下調(diào)用下一層phase來查找動(dòng)態(tài)庫的路徑, 直到loadPhase6(),查找的順序?yàn)?code>DYLD_ROOT_PATH->LD_LIBRARY_PATH->DYLD_FRAMEWORK_PATH->原始路徑->DYLD_FALLBACK_LIBRARY_PATH,找到后調(diào)用ImageLoaderMachO::instantiateFromFile()來實(shí)例化一個(gè)ImageLoader, 之后調(diào)用checkandAddImage()驗(yàn)證映像并將其加入到全局映像列表中. 如果loadPhase0()返回為空, 表示在路徑中沒有找到動(dòng)態(tài)庫, 就嘗試從共享緩存中查找, 找到就調(diào)用ImageLoaderMachO::instantiateFromCache()從緩存中加載, 否則就拋出沒找到映像的異常.

5 鏈接主程序

這一步執(zhí)行link()完成主程序的鏈接操作, 該函數(shù)調(diào)用了ImageLoader自身的link()函數(shù), 主要的目的是將實(shí)例化的主程序的動(dòng)態(tài)數(shù)據(jù)進(jìn)行修正, 達(dá)到讓進(jìn)程可用的目的, 典型的就是主程序中的符號表修正操作.

6 鏈接插入的動(dòng)態(tài)庫

鏈接插入的動(dòng)態(tài)庫與鏈接主程序一樣, 都是使用的link(), 插入的動(dòng)態(tài)庫列表是前面調(diào)用addImage()保存到sAllImages中的, 之后, 循環(huán)獲取每一個(gè)動(dòng)態(tài)庫的ImageLoader, 調(diào)用link()對其進(jìn)行鏈接, 注意: sAllImages中保存的第一項(xiàng)是主程序的映像. 接下來調(diào)用每個(gè)映像的registerInterposing()方法來注冊動(dòng)態(tài)庫插入與調(diào)用applyInterposing()應(yīng)用插入操作.

registerInterposing()查找__DATA段__interpose節(jié)區(qū), 找到需要應(yīng)用插入操作(也可以叫作符號地址替換)的數(shù)據(jù), 然后做一些檢查后, 將要替換的符號與被替換的符號信息存入fgInterposingTuples列表中, 供以后具體符號替換時(shí)查詢. applyInterposing()調(diào)用了虛方法doInterpose()來做符號替換操作, 在ImageLoaderMachOCompressed中實(shí)際是調(diào)用了eachBind()eachLazyBind()分別對常規(guī)的符號與延遲加載的符號進(jìn)行應(yīng)用插入操作, 具體使用的是interposeAt(), 該方法調(diào)用interposedAddress()fgInterposingTuples中查找要替換的符號地址, 找到后然后進(jìn)行最終的符號地址替換.

注意是libSystem就是此時(shí)加入的, runtime的初始化函數(shù)_objc_init()就是libSystem調(diào)用的

7 執(zhí)行弱符號綁定

weakBind()函數(shù)執(zhí)行弱符號綁定. 首先通過調(diào)用context的getCoalescedImages()sAllImages中所有含有弱符號的映像合并成一個(gè)列表,合并完后調(diào)用initializeCoalIterator()對映像進(jìn)行排序,排序完成后調(diào)用incrementCoalIterator()收集需要進(jìn)行綁定的弱符號,后者是一個(gè)虛函數(shù),在ImageLoaderMachOCompressed中,該函數(shù)讀取映像動(dòng)態(tài)鏈接信息的weak_bind_offweak_bind_size來確定弱符號的數(shù)據(jù)偏移與大小,然后挨個(gè)計(jì)算它們的地址信息. 之后調(diào)用getAddressCoalIterator(),按照映像的加載順序在導(dǎo)出表中查找符號的地址,找到后調(diào)用updateUsesCoalIterator()執(zhí)行最終的綁定操作,執(zhí)行綁定的是bindLocation(),前面有講過,此處不再贅述.

8 執(zhí)行初始化方法

執(zhí)行初始化的方法是:

  1. initializeMainExecutable()
  2. ImageLoader::runInitializers()
  3. ImageLoader::processInitializers()
  4. ImageLoader::recursiveInitialization()
  5. 先調(diào)用image自己的初始化函數(shù)initializer, 然后發(fā)出通知, 告知觀察者當(dāng)前image已經(jīng)完成加載, 狀態(tài)是dyld_image_state_initialized
    • doInitialization
      • doImageInit, 獲取mach-o的init方法的地址并調(diào)用
      • doModInitFunctions, 獲取mach-o的static initializer的地址并調(diào)用
    • context.notifySingle(dyld_image_state_initialized, this), 對外發(fā)出通知,當(dāng)前image已經(jīng)初始化完畢.
9 APP的main函數(shù)地址入口點(diǎn)并返回

這一步調(diào)用主程序映像的getThreadPC()函數(shù)來查找主程序的LC_MAIN加載命令獲取程序的入口點(diǎn), 沒找到就調(diào)用getMain()LC_UNIXTHREAD加載命令中去找, 找到后就跳到入口點(diǎn)指定的地址, dyld::main函數(shù)會(huì)返回程序的main函數(shù)地址, main函數(shù)被調(diào)用, 從而代碼來到了我們熟悉的程序入口。

到這里, dyld整個(gè)加載動(dòng)態(tài)庫的過程就算完成了。

關(guān)于initializeMainExecutable方法中, image的初始化詳解

前面一節(jié)中第8步的內(nèi)容, 是針對所有image執(zhí)行initializeMainExecutable方法, 具體的代碼如下:

/**
 initializeMainExecutable 執(zhí)行初始化方法,其中 +load 和 constructor 方法就是在這里執(zhí)行。
 initializeMainExecutable 內(nèi)部先調(diào)用了動(dòng)態(tài)庫的初始化方法,后調(diào)用主程序的初始化方法。
 */
void initializeMainExecutable() {
    // record that we've reached this step
    // 進(jìn)行標(biāo)志 gLinkContext, 已經(jīng)開始 initialize MainExecutable這個(gè)image.
    gLinkContext.startedInitializingMainExecutable = true;

    // run initialzers for any inserted dylibs
    // 給被插入的所有的 dylibs 進(jìn)行初始化 -- 調(diào)用 initialzers
    ImageLoader::InitializerTimingList initializerTimes[sAllImages.size()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        // 從 sImageRoots 中的第一個(gè)變量是 MainExcutable image, 因此這里初始化的時(shí)候需要跳過第一個(gè)數(shù)據(jù), 對其他后面插入的dylib進(jìn)行調(diào)用ImageLoader::runInitializers進(jìn)行初始化
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }

    // 單獨(dú)對 main executable調(diào)用ImageLoader::runInitializers進(jìn)行初始化
    // 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]);
}

其中, 主要是調(diào)用所有imageImageLoader::runInitializers()方法, 代碼如下:

/**
 初始化過程如下, 實(shí)際調(diào)用ImageLoader::processInitializers
 @param context
 @param timingInfo
 */
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo) {
    // 當(dāng)前系統(tǒng)時(shí)間
    uint64_t t1 = mach_absolute_time();
    // 當(dāng)前調(diào)用線程
    mach_port_t thisThread = mach_thread_self();
    ImageLoader::UninitedUpwards up;
    up.count = 1;
    up.images[0] = this;

    // 初始化當(dāng)前 imageLoader 中的 image鏡像的實(shí)際調(diào)用方法 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.
// 調(diào)用 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.
    // 處理當(dāng)前image依賴 dylib動(dòng)態(tài)庫, 調(diào)用 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方法中, 會(huì)遞歸的調(diào)用當(dāng)前image的依賴的dylib動(dòng)態(tài)庫的初始化函數(shù)進(jìn)行初始化, 然后才調(diào)用doInitialization來調(diào)用自己的初始化函數(shù), 在中間image的state狀態(tài)切換時(shí),對外通過notifySingle方法給外部環(huán)境contenxt發(fā)出狀態(tài)變化的通知(外部如果有內(nèi)容監(jiān)聽了相關(guān)通知, 那么會(huì)執(zhí)行相應(yīng)回調(diào), runtime就是如此):

/**
 遞歸調(diào)用 image 進(jìn)行初始化, 先調(diào)用image依賴的image進(jìn)行初始化. 直到自己
 */
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    recursive_lock lock_info(this_thread);
    recursiveSpinLock(lock_info);

    // 當(dāng)前ImageLoader依賴的Image還沒有初始化完, 進(jìn)入if中, 如果執(zhí)行完成, 直接返回
    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        uint8_t oldState = fState;
        // break cycles -> 這是設(shè)置當(dāng)前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底層的依賴庫都遞歸調(diào)用, 初始化完成.

            // 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, 當(dāng)前狀態(tài)發(fā)生變化 -- image的依賴已經(jīng)完全加載. 注意這里可能在runtime中注冊了狀態(tài)監(jiān)聽, 注冊了callback函數(shù), 當(dāng)狀態(tài)發(fā)送變化時(shí), 會(huì)觸發(fā)回調(diào)函數(shù).
            context.notifySingle(dyld_image_state_dependents_initialized, this);

            // 初始化當(dāng)前image, `ImageLoaderMachO::doInitialization`方法內(nèi)部會(huì)調(diào)用image的"Initializer", 這是一個(gè)函數(shù)指針, 實(shí)際是image的初始化方法. 例如 `libSystem.dylib`, 它的初始化方法就比較特殊, 我們可以參考libSystem的init.c源碼, 內(nèi)部的`libsystem_initializer`函數(shù)就是初始化真正調(diào)用的函數(shù)

            //  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, 檔期那狀態(tài)發(fā)送變化 -- image自己已經(jīng)完成初始化!!!!
            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方法, 它會(huì)調(diào)用doImageInitdoModInitFunctions方法, 這兩個(gè)方法做的工作就是從image鏡像中獲取這個(gè)鏡像的真正的入口初始化方法initializer并調(diào)用.

注意, initalizer并非名為initalizer的方法, 而是C++靜態(tài)對象初始化構(gòu)造器, atribute((constructor) 進(jìn)行修飾的方法, 在ImageLoader類中initializer函數(shù)指針?biāo)赶蛟摮跏蓟椒ǖ牡刂?/p>

/**
 image 真正的初始化調(diào)用方法, 內(nèi)部會(huì)調(diào)用`doImageInit`, 這個(gè)方法會(huì)調(diào)用 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函數(shù)指針的的定義和調(diào)用的過程, 首先Initializer是一個(gè)函數(shù)指針, 在doModInitFunctions方法中是獲取image中Initializer方法地址, 然后直接執(zhí)行.

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);
    ...
}

在最后會(huì)調(diào)用notifySingle通知方法, 會(huì)觸發(fā)外部監(jiān)聽dyld_image_state_initialized狀態(tài)的callback回調(diào)方法.

libdiaptch的初始化和runtime的初始化

上一節(jié)中, 我們知道一個(gè)image動(dòng)態(tài)庫要被初始化并且加載完成, 需要先遞歸加載它依賴的image動(dòng)態(tài)庫.

那么一個(gè)最簡單的iOS程序依賴哪些動(dòng)態(tài)庫呢?

我們可以在一個(gè)iOS APP中設(shè)置環(huán)境變量DYLD_PRINT_INITIALIZERS為1, 然后編譯運(yùn)行, 控制臺(tái)就會(huì)打印出當(dāng)前APP依賴的所有的動(dòng)態(tài)庫.

具體步驟是: Edit-Scheme->Arguments->Enviroments Vairables增加一個(gè)參數(shù) --- Name: DYLD_PRINT_INITIALIZERS, Value: 1, 然后編譯, 控制臺(tái)會(huì)打印如下內(nèi)容:

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

我們看到第一個(gè)initializer functionlibSystem.B.dylib.

我們在Apple官網(wǎng)找到libSystem.dylib開源庫(可能和實(shí)際有所不同), 我們看一下它的初始化方法具體做了什么工作, 我們找到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)
{
    ...

    // 內(nèi)核初始化
    __libkernel_init(&libkernel_funcs, envp, apple, vars);
    // 平臺(tái)信息初始化
    __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里調(diào)用了到了runtime初始化方法_objc_init.我們可以、在程序中打個(gè)符號斷點(diǎn)來驗(yàn)證:
    libdispatch_init();
    _libxpc_initializer();

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

libdispatch_init里調(diào)用了到了runtime初始化方法_objc_init.

我們可以、在程序中打個(gè)符號斷點(diǎn)來驗(yàn)證, 在Xcode設(shè)置Symbolic Breakpoint, 然后設(shè)置Symblic = _objc_init:

Apple官網(wǎng)開源了libsystem的源碼
https://opensource.apple.com/tarballs/Libsystem/

libSystem_init.jpg

從圖中我們看到, 先libSystem_initializer調(diào)用libdispatch_init再到_objc_init初始化runtime

runtime初始化后不會(huì)閑著, 在_objc_init中注冊了幾個(gè)通知, 從dyld這里接手了幾個(gè)活, 其中包括負(fù)責(zé)初始化相應(yīng)依賴庫里的類結(jié)構(gòu), 調(diào)用依賴庫里所有的+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 方法是如何被調(diào)用的?

 A:_objc_init(OBJC runtime)是 libSystem 在library初始化之前調(diào)用, 進(jìn)行runtime的初始化!!!! 當(dāng) Objective-C runtime初始化的時(shí)候, 會(huì)通過 dyld_register_image_state_change_handler注冊針對一些事件的監(jiān)聽, 在每次有新的鏡像加入運(yùn)行時(shí)的時(shí)候,進(jìn)行回調(diào)調(diào)用`load_images`(也有其他事件觸發(fā) map_images和 unmap_image方法).

 執(zhí)行 load_images 將所有包含 load 方法的文件加入列表 loadable_classes ,然后從這個(gè)列表中找到對應(yīng)的 load 方法的實(shí)現(xiàn),調(diào)用 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中加入一個(gè)狀態(tài)監(jiān)聽器, 一旦dyld監(jiān)聽到有新的鏡像image加載完成, 就調(diào)用 load_images 方法, 并傳入最新鏡像的信息類別 infoList
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

前面說到image在被dyld加載的時(shí)候, notifySingle 會(huì)通知image的狀態(tài)變化, 這樣_objc_init中最后通過_dyld_objc_notify_register方法注冊的監(jiān)聽器, 后面一旦有新的image被加載到dyld時(shí), 就會(huì)調(diào)用objc::load_images方法!!!

關(guān)于runtime中的load_images, 其他文章會(huì)進(jìn)行詳解解答后面的內(nèi)容.

啟動(dòng)流程總結(jié)

一個(gè) iOS 程序會(huì)依賴于某些動(dòng)態(tài)庫, 可以通過otool -v -L debug-objc命令查看, 在啟動(dòng)的時(shí)候需要由動(dòng)態(tài)鏈接器進(jìn)行動(dòng)態(tài)鏈接,而動(dòng)態(tài)鏈接器自身也是動(dòng)態(tài)庫,因此動(dòng)態(tài)鏈接器會(huì)首先自舉自身,然后再將程序依賴的動(dòng)態(tài)庫依次加載進(jìn)內(nèi)存進(jìn)行鏈接,動(dòng)態(tài)鏈接做完之后,把控制權(quán)交給程序的main函數(shù)。

通過 otool 命令可以查看到依賴列表中有 /usr/lib/libSystem.B.dylib/usr/lib/libobjc.A.dylib 這樣兩條記錄.libSystem.B.dylib 在編譯的時(shí)候依賴于很多系統(tǒng)級別的 lib, 如 libdispatch,libsystem_c, libcommonCrypto等等.而這個(gè)libobjc.A.dylib就是objc-runtime, 其初始化函數(shù)是 _objc_init, 前面說過, 可以根據(jù)_objc_init設(shè)置symbol breakpoint進(jìn)行后續(xù)的調(diào)試.

  1. 首先系統(tǒng)內(nèi)核建立進(jìn)程, 創(chuàng)建虛擬空間, 讀取 Mach-O 文件頭,并建立虛擬空間與Mach-O文件的映射關(guān)系, 然后找到 dyld 動(dòng)態(tài)鏈接器入口 _dyld_start 之后, 控制權(quán)交給動(dòng)態(tài)鏈接器, 接下來就是 dyld 的自舉, 即 dyldbootstrap::start().
  2. 自舉過程完成, 即在 dyldbootstrap::start() 函數(shù)的最末尾會(huì)調(diào)用 dyld::_main 函數(shù), 并把此函數(shù)的返回值返還給 _dyld_start, 這個(gè)返回值是 iOS 可執(zhí)行程序main函數(shù)的地址,_dyld_start 會(huì)跳到該地址繼續(xù)執(zhí)行,進(jìn)入程序的主邏輯.
  3. 不過在這個(gè)地址返回之前, 也就是在dyld::_main函數(shù)中需要做兩個(gè)特別重要的工作:
    • 重定位和初始化, 也就是動(dòng)態(tài)鏈接的過程. 在完成了自舉之后, 鏈接器需要裝載動(dòng)態(tài)鏈接庫, 然后完成符號的重定位.
    • 如果動(dòng)態(tài)鏈接庫文件中還有相應(yīng)的初始化信息,即含有 __mod_init_func section,就再需要調(diào)用其相應(yīng)的初始化函數(shù).
  4. dyld::initializeMainExecutable函數(shù)中, 程序所依賴的動(dòng)態(tài)庫進(jìn)行各自的初始化, 因此ImageLoader::runInitializers函數(shù)被調(diào)用, 這個(gè)函數(shù)調(diào)用ImageLoader::processInitializers來處理初始化, 而某個(gè)動(dòng)態(tài)庫可能依賴于其它的動(dòng)態(tài)庫, 那么它所依賴的動(dòng)態(tài)庫就需要先初始化, 這里形成一個(gè)遞歸, 也就是ImageLoader::recursiveInitialization
  5. 動(dòng)態(tài)庫初始化函數(shù)的真正調(diào)用是在 ImageLoaderMachO::doModInitFunctions函數(shù)中, 對于 libSystem.B.dylib來說其初始化函數(shù)是 libSystem_initializer, 在這個(gè)函數(shù)中libdispatch_init被調(diào)用, libSystem 以及 libdispatch 也是開源的, 可以查看相關(guān)源碼.
  6. libdispatch_init中, 對 Runtime_objc_init進(jìn)行了調(diào)用, 而在 Runtime 的初始化過程中, 查看源碼可以看到 Runtimedyld 綁定了回調(diào), 當(dāng) image 加載到內(nèi)存后, dyld 會(huì)通知 Runtime 進(jìn)行處理, Runtime 接手后調(diào)用 map_images 做解析和處理, 把Category 的實(shí)例方法, 協(xié)議以及屬性添加到類上, 把 Category 的類方法和協(xié)議添加到類的 metaclass 上; 接下來 load_images 中調(diào)用 call_load_methods 方法, 遍歷所有加載進(jìn)來的 Class, 按繼承層級依次調(diào)用 Classload 方法和其 Categoryload 方法(這部分內(nèi)容,會(huì)在后面針對runtime的文章進(jìn)行分析)
  7. 在所有的動(dòng)態(tài)庫做好符號重定位和初始化工作之后, 也就是 dyld::_main 臨近末尾的時(shí)候,dyld 會(huì)獲取 main 函數(shù)的地址返回給 dyld, dyld 緊接著調(diào)用 main 函數(shù), 將控制權(quán)交換給主程序, 程序開始真正的執(zhí)行.

結(jié)語

最后一句話概括APP的啟動(dòng):

內(nèi)核exec做好準(zhǔn)備工作,轉(zhuǎn)移控制權(quán)給dyld->dyld加載依賴庫->libdispatch初始化->runtime初始化->main開始執(zhí)行

APP的啟動(dòng)優(yōu)化

可以參考: http://www.itdecent.cn/p/7096478ccbe7

參考文章

http://www.itdecent.cn/p/7096478ccbe7
http://www.itdecent.cn/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
http://www.itdecent.cn/p/885c8077b27d
https://www.cnblogs.com/maizi008/p/5086103.html
https://blog.csdn.net/TuGeLe/article/details/81609604
http://www.itdecent.cn/p/5f337da8fbef
http://www.itdecent.cn/p/231b1cebf477
https://www.cnblogs.com/xs514521/p/7010458.html
http://www.itdecent.cn/p/73a99303cd91

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容