+load和main()誰先調用?有經驗的iOSer們會絕不猶豫的回答出來是load方法,但爲何是load方法呢?今天咱們來探討一下底層的原理html
新建一個項目,在AppDelegate裏添加load方法,打上一個斷點就會看到以下圖所示的調用堆棧,若是嫌左側太長了看不全,也能夠在控制檯輸入bt指令查看調用堆棧 bootstrap
從調用堆棧中咱們能夠看到,程序由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_start
session
_dyld_start
從截圖中能夠看到,搜索結果的第一部分和第二部分,都不多是咱們想要找的東西,第一部分.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(很快就找到了 函數的return的前面一行有一個appsSlide變量,這個變量是ASLR地址空間配置隨機加載技術的應用,這是是一種防範內存損壞漏洞被利用的計算機安全技術
這個start函數裏面的最後一行代碼調用了_main()函數,這個比較舒服,不須要咱們再去找了,直接按住command而後左鍵點擊就能夠跳轉到對應的實現代碼了架構
_main
這個_main函數就是啓動咱們APP的關鍵代碼,從它的行數就能夠看出來它的份量了,從6455行到7303一共848行(這裏就不貼它的所有源碼了)好傢伙,仍是頭一回看到一個C函數寫這麼長的,原本是想吐槽一下的,但一想到這是蘋果的工程師寫的底層代碼,咱仍是老老實實看源碼...這部分的代碼就是今天重點中的重點,咱們在下一部分重點講這個函數,如今仍是接着調用堆棧繼續日後面走,後面的幾個都挺好找的 app
initializeMainExecutable
這個initializeMainExecutable
函數在_main
裏面調用了兩次,不過是分不一樣架構的,也是能夠直接command加左鍵定位到函數實現的,從這個函數裏的註釋中能夠看到,是先執行的全部插入的庫的initialzers方法,再執行咱們主程序的initialzers方法的,這也說明了咱們寫的Framework中的load方法會比咱們主程序的load方法先執行;若是不認爲run initialzers就是調用load方法,能夠跟着調用堆棧流程走完,你就會知道究竟是不是了 ide
runInitializers
這個runInitializers
函數也是在上面initializeMainExecutable
函數裏面調用了兩次,從兩次調用的註釋來看,上面調用的是全部插入的動態庫的初始化方法,而下面的纔是咱們主程序調用初始化方法,因此在[AppDelegate laod]
方法中打的斷點卡住的應該是下面的這段代碼;這個函數也比較好找,直接搜runInitializers(只有12個結果在5個文件裏,載結合它前面的ImageLoader做用域,函數的參數,很快就能找到它的實現
processInitializers
結合調用堆棧,找到runInitializers
裏面的processInitializers
函數實現很容易,找到processInitializers
函數的實現也很簡單,能夠直接command加左鍵點擊就到了
recursiveInitialization
一樣是在ImageLoader.cpp文件內,processInitializers
就能直接command加左鍵定位到實現代碼,而recursiveInitialization
卻不能夠,不知道爲何,不過也沒什麼大問題,recursiveInitialization
在當前文件一搜就找到實現了 這裏有個小細節能夠說一下,在第一次content.notifySingle()以後有一個
doInitialization
函數,這個函數 裏面又會有兩個初始化函數 doInitialization()內部首先調用doImageInit來執行鏡像的初始化函數,也就是LC_ROUTINES_COMMAND中記錄的函數。 再執行doModInitFunctions()方法來解析並執行
_DATA_,__mod_init_func
這個section中保存的函數。使用__attribute__((constructor))
開頭的C函數會保存在這裏面,如圖所示:
notifySingle
notifySingle
的調用一樣是在上一個函數recursiveInitialization
的實現裏面,可是notifySingle
的實現結合調用堆棧它前面的做用域來看,不在ImageLoader裏面,那就直接全局搜索notifySingle(,也比較容易找 接下來,由
notifySingle
到load_images
會發現調用的地方已經不是在dyld了...那麼如何實現代碼的執行從一個程序跳到另外一個程序呢?有不少種辦法,通知,代理,block,函數做爲參數傳遞,咱們仔細觀察一下notifySingle
裏面有沒有以上任何一種,會發現下面這裏有一個不太同樣的地方sNotifyObjCInit
那接下來就看看這個
sNotifyObjCInit
變量是在哪裏被賦值的,搜索一番後發如今這裏被賦值了 緊接着搜一搜這個
registerObjCNotifiers
在哪裏被調用了 搜索一番後發現這裏是dyld提供的對外部的接口...那就說明咱們在dyld裏面應該是找不到這個調用的地方了,不過至少咱們找到了這個對外的接口函數
_dyld_objc_notify_register
,這個時候,咱們能夠回到最開始新建的項目中去,下一個_dyld_objc_notify_register
的符號斷點 會發現是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目錄,是否是能夠看到一個很是眼熟的東西
這樣看來咱們真的找對地方了,那就直接全局搜索咱們剛剛獲取到的_dyld_objc_notify_register
函數,看看是否是_objc_init
裏面調用了它 果真如此,看到這裏,就會發現dyld裏面的
sNotifyObjCInit
變量是被賦值了一個load_images
的函數,到這裏就徹底能解釋的通,從調用堆棧的第2幀到第1幀的執行了,這個load_images
能夠直接按住command點擊定位到實現代碼
load_images
接下來就是看怎麼從libobjc.A.dylib的load_images函數到咱們的斷點[AppDelegate load]
方法的了 很明顯,咱們須要查看
call_load_methods
函數 在這個函數裏,咱們就能夠明顯的看到先是調用全部類Class的load方法,而後再調用全部分類category中的load方法,查看
call_class_loads
的實現 到這裏,咱們斷點卡住的全部調用堆棧就所有跟蹤完畢了
須要瞭解的是,這個時候咱們依然還在dyld的_main
函數裏面,連_main
裏面的initializeMainExecutable
都沒有執行完...而咱們主程序的main()是在何時調用的呢?dyld的_main
函數的返回值result就是咱們主程序的入口,_main
執行完畢以後,會把返回值返回到start
函數,start
函數又會把返回值返回到咱們的最初的入口_dyld_start
裏面的那條bl
指令後,x0
就是返回的咱們主程序入口,接下來一個mov x16,x0
看註釋也知道是將主程序入口地址保存到x16
了,再搜索一下x16
發現後面基本都是各類狀況下的br
或者braaz
到x16
,就是跳轉到咱們的主程序了,因此咱們APP的main函數遠遠晚於load方法的調用
上面調用堆棧裏這麼多函數裏面,最複雜最長的就是這個_main
了,其實這個函數裏面纔是dyld啓動咱們APP的主要流程,可是這個函數實在太長了,咱們將這個函數分紅9個主要的部分,我向來討厭在文章裏面貼那種幾頁幾頁都翻不完的代碼塊的,因此下面的介紹都儘可能精簡
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;
......
複製代碼
configureProcessRestrictions()用來配置進程是否受限
checkEnvironmentVariables()檢查環境變量
細心的讀者可能會注意到,整個過程當中有一些DYLD_PRINT_開頭的環境變量,好比:
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
複製代碼
若是在Xcode中配置了這些環境變量,就會在咱們的控制檯中打印相關的信息: 除了上面的兩個外,我下面以另一個打印啓動時間的環境變量DYLD_PRINT_STATISTICS_DETAILS
爲例,這個應該能夠做爲優化啓動時間的參考數據
這裏先說明一下,iOS的共享緩存機制:在iOS系統中,每一個程序依賴的動態庫都須要經過dyld一個個加載到內存,然而,不少系統庫基本上是每一個程序都會用到的(好比UIKit,Foundation...),若是每一個程序啓動運行的時候都重複的去加載一次,勢必會形成運行緩慢,沒必要要的內存消耗,爲了優化啓動速度和節約內存消耗,共享緩存機制就出現了;這裏還要說一句,在iOS中,只有系統庫才能稱爲真正意義上的動態庫,咱們普通開發者開發的各類格式的庫,都只能是靜態庫;全部默認的動態連接庫被合併成一個大的緩存文件,放在/System/Library/Caches/com.apple.dyld/
目錄下,按不一樣的架構分別保存,想要分析某個系統庫,能夠從dyld_shared_cache裏將原始的二進制文件提取出來;感興趣的能夠根據個人第一篇參考文章上的步驟去嘗試一下
這一步先調用checkSharedRegionDisable()檢查共享緩存是否禁用。該函數的iOS實現部分僅有一句註釋,從註釋咱們能夠推斷iOS必須開啓共享緩存才能正常工做,代碼以下: 接下來調用mapSharedCache()加載共享緩存,而mapSharedCache()裏面實則是調用了loadDyldCache(),從代碼能夠看出,共享緩存加載又分爲三種狀況:
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()裏面就是具體的共享緩存解析邏輯,感興趣的讀者能夠詳細分析。
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映射到申請的內存中;至此,初始化主程序這一步就完成了
這一步是加載環境變量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);
}
複製代碼
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
複製代碼
這一步調用link()函數將實例化後的主程序進行動態修正,讓二進制變爲可正常執行的狀態。link()函數內部調用了ImageLoader::link()函數,從源代碼能夠看到,這一步主要作了如下幾個事情:
這一步與連接主程序同樣,將前面調用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);
}
}
}
複製代碼
initializeMainExecutable
initializeMainExecutable();
複製代碼
這個函數在咱們剛剛的調用堆棧流程跟蹤裏面講到過,咱們Objective-C對象的load方法,庫中的load方法,還有C++的初始化方法都在這裏面被執行了
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
在 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 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 closures「是否正確,把dylib映射到APP進程的地址空間裏,而後跳轉到main函數。此時,它再也不須要分析mach-o header和執行符號查找,節省了很多時間。
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有深刻的理解。
這篇文章主要參考瞭如下幾篇文章: