本文的目的主要是分析dyld的加載流程,了解在main函數(shù)之前,底層還做了什么
引子
-
創(chuàng)建一個project,在
ViewController中重寫了load方法,在main中加了一個C++方法,即kcFUnc,請問它們的打印先后順序是什么?
問題引入 -
運行程序,查看 load、kcFunc、main的
打印順序,下面是打印結果,通過結果可以看出其順序是load --> C++方法 --> main
打印結果
為什么是這么一個順序?按照常規(guī)的思維理解,main不是入口函數(shù)嗎?為什么不是main最先執(zhí)行?
下面根據(jù)這個問題,我們來探索在走到main之前,到底還做了什么。
編譯過程及庫
在分析app啟動之前,我們需要先了解iOSapp代碼的編譯過程以及動態(tài)庫和靜態(tài)庫。
編譯過程
其中編譯過程如下圖所示,主要分為以下幾步:
-
源文件:載入.h、.m、.cpp等文件 -
預處理:替換宏,刪除注釋,展開頭文件,產(chǎn)生.i文件 -
編譯:將.i文件轉換為匯編語言,產(chǎn)生.s文件 -
匯編:將匯編文件轉換為機器碼文件,產(chǎn)生.o文件 -
鏈接:對.o文件中引用其他庫的地方進行引用,生成最后的可執(zhí)行文件
編譯過程
靜態(tài)庫 和 動態(tài)庫
-
靜態(tài)庫:在鏈接階段,會將可匯編生成的目標程序與引用的庫一起鏈接打包到可執(zhí)行文件當中。此時的靜態(tài)庫就不會在改變了,因為它是編譯時被直接拷貝一份,復制到目標程序里的好處:編譯完成后,庫文件實際上就沒有作用了,目標程序沒有外部依賴,直接就可以運行缺點:由于靜態(tài)庫會有兩份,所以會導致目標程序的體積增大,對內存、性能、速度消耗很大
-
動態(tài)庫:程序編譯時并不會鏈接到目標程序中,目標程序只會存儲指向動態(tài)庫的引用,在程序運行時才被載入-
優(yōu)勢:減少打包之后app的大小:因為不需要拷貝至目標程序中,所以不會影響目標程序的體積,與靜態(tài)庫相比,減少了app的體積大小共享內存,節(jié)約資源:同一份庫可以被多個程序使用通過
更新動態(tài)庫,達到更新程序的目的:由于運行時才載入的特性,可以隨時對庫進行替換,而不需要重新編譯代碼
-
缺點:動態(tài)載入會帶來一部分性能損失,使用動態(tài)庫也會使得程序依賴于外部環(huán)境,如果環(huán)境缺少了動態(tài)庫,或者庫的版本不正確,就會導致程序無法運行
-
靜態(tài)庫和動態(tài)庫的圖示如圖所示

dyld加載流程分析
根據(jù)dyld源碼,以及libobjc、libSystem、libdispatch源碼協(xié)同分析
什么是dyld?
dyld(the dynamic link editor)是蘋果的動態(tài)鏈接器,是蘋果操作系統(tǒng)的重要組成部分,在app被編譯打包成可執(zhí)行文件格式的Mach-O文件后,交由dyld負責連接,加載程序
所以 App的啟動流程圖如下

app啟動的起始點
-
在前文的demo中,在
load方法處加一個斷點,通過bt堆棧信息查看app啟動是從哪里開始的
app啟動起點
【app啟動起點】:通過程序運行發(fā)現(xiàn),是從dyld中的_dyld_start開始的,所以需要去OpenSource下載一份dyld的源碼來進行分析 -
也可以通過xcode左側的堆棧信息來找到入口
xcode堆棧信息
dyld::_main函數(shù)源碼分析
-
在
dyld-750.6源碼中查找_dyld_start,查找arm64架構發(fā)現(xiàn),是由匯編實現(xiàn),通過匯編注釋發(fā)現(xiàn)會調用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)方法,是一個C++方法(以arm64架構為例)
dyldbootstrap::start源碼 -
源碼中搜索
dyldbootstrap找到命名作用空間,再在這個文件中查找start方法,其核心是返回值的調用了dyld的main函數(shù),其中macho_header是Mach-O的頭部,而dyld加載的文件就是Mach-O類型的,即Mach-O類型是可執(zhí)行文件類型,由四部分組成:Mach-O頭部、Load Command、section、Other Data,可以通過MachOView查看可執(zhí)行文件信息
_main源碼實現(xiàn) -
進入
dyld::_main的源碼實現(xiàn),特別長,大約600多行,如果對dyld加載流程不太了解的童鞋,可以根據(jù)_main函數(shù)的返回值進行反推,這里就多作說明。在_main函數(shù)中主要做了一下幾件事情:-
【第一步:
環(huán)境變量配置】:根據(jù)環(huán)境變量設置相應的值以及獲取當前運行架構
第一步 -
【第二步:
共享緩存】:檢查是否開啟了共享緩存,以及共享緩存是否映射到共享區(qū)域,例如UIKit、CoreFoundation等
第二步 -
【第三步:
主程序的初始化】:調用instantiateFromLoadedImage函數(shù)實例化了一個ImageLoader對象
第三步 -
【第四步:
插入動態(tài)庫】:遍歷DYLD_INSERT_LIBRARIES環(huán)境變量,調用loadInsertedDylib加載
第四步 -
【第五步:
link 主程序】
第五步 -
【第六步:
link 動態(tài)庫】
第六步 -
【第七步:
弱符號綁定】
第七步 -
【第八步:
執(zhí)行初始化方法】
第八步 -
【第九步:
尋找主程序入口即main函數(shù)】:從Load Command讀取LC_MAIN入口,如果沒有,就讀取LC_UNIXTHREAD,這樣就來到了日常開發(fā)中熟悉的main函數(shù)了
第九步
-
下面主要分析下【第三步】和【第八步】
第三步:主程序初始化
-
sMainExecutable表示主程序變量,查看其賦值,是通過instantiateFromLoadedImage方法初始化
instantiateFromLoadedImage初始化主程序 -
進入
instantiateFromLoadedImage源碼,其中創(chuàng)建一個ImageLoader實例對象,通過instantiateMainExecutable方法創(chuàng)建
instantiateFromLoadedImage源碼實現(xiàn) -
進入
instantiateMainExecutable源碼,其作用是為主可執(zhí)行文件創(chuàng)建映像,返回一個ImageLoader類型的image對象,即主程序。其中sniffLoadCommands函數(shù)時獲取Mach-O類型文件的Load Command的相關信息,并對其進行各種校驗
instantiateMainExecutable源碼實現(xiàn)
第八步:執(zhí)行初始化方法
-
進入
initializeMainExecutable源碼,主要是循環(huán)遍歷,都會執(zhí)行runInitializers方法
initializeMainExecutable源碼實現(xiàn) -
全局搜索
runInitializers(cons,找到如下源碼,其核心代碼是processInitializers函數(shù)的調用
runInitializers源碼實現(xiàn) -
進入
processInitializers函數(shù)的源碼實現(xiàn),其中對鏡像列表調用recursiveInitialization函數(shù)進行遞歸實例化
processInitializers源碼實現(xiàn) -
全局搜索
recursiveInitialization(cons函數(shù),其源碼實現(xiàn)如下
recursiveInitialization源碼實現(xiàn)
在這里,需要分成兩部分探索,一部分是notifySingle函數(shù),一部分是doInitialization函數(shù),首先探索notifySingle函數(shù)
notifySingle 函數(shù)
-
全局搜索
notifySingle(函數(shù),其重點是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());這句
notifySingle源碼實現(xiàn) -
全局搜索
sNotifyObjCInit,發(fā)現(xiàn)沒有找到實現(xiàn),有賦值操作
registerObjCNotifiers源碼實現(xiàn) -
搜索
registerObjCNotifiers在哪里調用了,發(fā)現(xiàn)在_dyld_objc_notify_register進行了調用
_dyld_objc_notify_register源碼實現(xiàn)
注意:_dyld_objc_notify_register的函數(shù)需要在libobjc源碼中搜索 -
在
objc4-781源碼中搜索_dyld_objc_notify_register,發(fā)現(xiàn)在_objc_init源碼中調用了該方法,并傳入了參數(shù),所以sNotifyObjCInit的賦值的就是objc中的load_images,而load_images會調用所有的+load方法。所以綜上所述,notifySingle是一個回調函數(shù)
_objc_init源碼實現(xiàn)
load函數(shù)加載
下面我們進入load_images的源碼看看其實現(xiàn),以此來證明load_images中調用了所有的load函數(shù)
-
通過objc源碼中_objc_init源碼實現(xiàn),進入
load_images的源碼實現(xiàn)
load_images源碼實現(xiàn) -
進入
call_load_methods源碼實現(xiàn),可以發(fā)現(xiàn)其核心是通過do-while循環(huán)調用+load方法
call_load_methods源碼實現(xiàn) -
進入
call_class_loads源碼實現(xiàn),了解到這里調用的load方法證實我們前文提及的類的load方法
call_class_loads源碼實現(xiàn)
所以,load_images調用了所有的load函數(shù),以上的源碼分析過程正好對應堆棧的打印信息

【總結】load的源碼鏈為:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一個回調處理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
那么問題又來了,_objc_init是什么時候調用的呢?請接著往下看
doInitialization 函數(shù)
-
走到
objc的_objc_init函數(shù),發(fā)現(xiàn)走不通了,我們回退到recursiveInitialization遞歸函數(shù)的源碼實現(xiàn),發(fā)現(xiàn)我們忽略了一個函數(shù)doInitialization
recursiveInitialization源碼實現(xiàn) -
進入
doInitialization函數(shù)的源碼實現(xiàn)
doInitialization源碼實現(xiàn)
這里也需要分成兩部分,一部分是doImageInit函數(shù),一部分是doModInitFunctions函數(shù)- 進入
doImageInit源碼實現(xiàn),其核心主要是for循環(huán)加載方法的調用,這里需要注意的一點是,libSystem的初始化必須先運行
doImageInit源碼實現(xiàn) - 進入
doModInitFunctions源碼實現(xiàn),這個方法中加載了所有Cxx文件
doModInitFunctions源碼實現(xiàn)
可以通過測試程序的堆棧信息來驗證,在C++方法處加一個斷點
C++斷點堆棧信息
- 進入
走到這里,還是沒有找到_objc_init的調用?怎么辦呢?放棄嗎?當然不行,我們還可以通過_objc_init加一個符號斷點來查看調用_objc_init前的堆棧信息,
-
_objc_init加一個符號斷點,運行程序,查看_objc_init斷住后的堆棧信息
_objc_init符號斷點堆棧信息 -
在
libsystem中查找libSystem_initializer,查看其中的實現(xiàn)
libSystem_initializer源碼實現(xiàn) -
根據(jù)前面的堆棧信息,我們發(fā)現(xiàn)走的是
libSystem_initializer中會調用libdispatch_init函數(shù),而這個函數(shù)的源碼是在libdispatch開源庫中的,在libdispatch中搜索libdispatch_init
libdispatch_init源碼實現(xiàn) -
進入
_os_object_init源碼實現(xiàn),其源碼實現(xiàn)調用了_objc_init函數(shù)
_os_object_init源碼實現(xiàn)
結合上面的分析,從初始化_objc_init注冊的_dyld_objc_notify_register的參數(shù)2,即load_images,到sNotifySingle-->sNotifyObjCInie=參數(shù)2到sNotifyObjcInit()調用,形成了一個閉環(huán)
所以可以簡單的理解為sNotifySingle這里是添加通知即addObserver,_objc_init中調用_dyld_objc_notify_register相當于發(fā)送通知,即push,而sNotifyObjcInit相當于通知的處理函數(shù),即selector
【總結】:_objc_init的源碼鏈:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)
第九步:尋找主入口函數(shù)
-
匯編調試,可以看到顯示來到
+[ViewController load]方法
匯編調試-load -
繼續(xù)執(zhí)行,來到
kcFunc的C++函數(shù)
匯編調試-kcFunc -
點擊
stepover,繼續(xù)往下,跑完了整個流程,會回到_dyld_start,然后調用main()函數(shù),通過匯編完成main的參數(shù)賦值等操作
匯編調試回到_dyld_start
dyld匯編源碼實現(xiàn)
dyld中main部分的匯編源碼實現(xiàn)
注意:
main是寫定的函數(shù),寫入內存,讀取到dyld,如果修改了main函數(shù)的名稱,會報錯
報錯信息
所以,綜上所述,最終dyld加載流程,如下圖所示,圖中也詮釋了前文中的問題:為什么是load-->Cxx-->main的調用順序












































