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

這篇文章主要參考瞭如下幾篇文章:

相關文章
相關標籤/搜索