本文的目的主要是分析dyld
的加載流程收苏,了解在main函數(shù)之前亿卤,底層還做了什么
引子
-
創(chuàng)建一個(gè)project,在
ViewController
中重寫了load
方法鹿霸,在main
中加了一個(gè)C++
方法怠噪,即kcFUnc
,請(qǐng)問它們的打印先后順序是什么杜跷? 運(yùn)行程序,查看 load矫夷、kcFunc葛闷、main的
打印順序
,下面是打印結(jié)果双藕,通過結(jié)果可以看出其順序是load --> C++方法 --> main
為什么是這么一個(gè)順序淑趾?按照常規(guī)的思維理解,main不是入口函數(shù)嗎忧陪?為什么不是main最先執(zhí)行?
下面根據(jù)這個(gè)問題嘶摊,我們來探索在走到main之前延蟹,到底還做了什么。
編譯過程&庫
在分析app啟動(dòng)之前叶堆,我們需要先了解iOS app的編譯過程
以及動(dòng)態(tài)庫
和靜態(tài)庫
阱飘。
編譯過程
其中編譯過程
如下圖所示,主要分為以下幾步:
源文件
:載入.h虱颗、.m沥匈、.cpp等文件預(yù)處理
:替換宏,刪除注釋忘渔,展開頭文件高帖,產(chǎn)生.i文件編譯
:將.i文件轉(zhuǎn)換為匯編語言,產(chǎn)生.s文件匯編
:將匯編文件轉(zhuǎn)換為機(jī)器碼文件畦粮,產(chǎn)生.o文件-
鏈接
:對(duì).o文件中引用其他庫的地方進(jìn)行引用散址,生成最后的可執(zhí)行文件
靜態(tài)庫 和 動(dòng)態(tài)庫
-
靜態(tài)庫
:在鏈接階段,會(huì)將可匯編生成的目標(biāo)程序與引用的庫一起鏈接打包到可執(zhí)行文件當(dāng)中宣赔。此時(shí)的靜態(tài)庫就不會(huì)在改變了爪飘,因?yàn)樗蔷幾g時(shí)被直接拷貝一份,復(fù)制到目標(biāo)程序里的
好處
:編譯完成后拉背,庫文件實(shí)際上就沒有作用了师崎,目標(biāo)程序沒有外部依賴,直接就可以運(yùn)行缺點(diǎn)
:由于靜態(tài)庫會(huì)有兩份椅棺,所以會(huì)導(dǎo)致目標(biāo)程序的體積增大
犁罩,對(duì)內(nèi)存齐蔽、性能、速度消耗很大
-
動(dòng)態(tài)庫
:程序編譯時(shí)并不會(huì)鏈接到目標(biāo)程序
中床估,目標(biāo)程序只會(huì)存儲(chǔ)指向動(dòng)態(tài)庫的引用含滴,在程序運(yùn)行時(shí)才被載入
-
優(yōu)勢(shì)
:減少打包之后app的大小
:因?yàn)椴恍枰截愔聊繕?biāo)程序中,所以不會(huì)影響目標(biāo)程序的體積丐巫,與靜態(tài)庫相比谈况,減少了app的體積大小共享內(nèi)存,節(jié)約資源
:同一份庫可以被多個(gè)程序使用通過
更新動(dòng)態(tài)庫递胧,達(dá)到更新程序
的目的:由于運(yùn)行時(shí)才載入的特性碑韵,可以隨時(shí)對(duì)庫進(jìn)行替換,而不需要重新編譯代碼
缺點(diǎn)
:動(dòng)態(tài)載入會(huì)帶來一部分性能損失
缎脾,使用動(dòng)態(tài)庫也會(huì)使得程序依賴于外部環(huán)境祝闻,如果環(huán)境缺少了動(dòng)態(tài)庫,或者庫的版本不正確遗菠,就會(huì)導(dǎo)致程序無法運(yùn)行
-
靜態(tài)庫和動(dòng)態(tài)庫的圖示如圖所示
dyld加載流程分析
根據(jù)dyld
源碼联喘,以及libobjc
、libSystem
辙纬、libdispatch
源碼協(xié)同分析
什么是dyld豁遭?
dyld
(the dynamic link editor)是蘋果的動(dòng)態(tài)鏈接器
,是蘋果操作系統(tǒng)的重要組成部分贺拣,在app被編譯打包成可執(zhí)行文件格式的Mach-O
文件后堤框,交由dyld負(fù)責(zé)連接,加載程序
所以 App的啟動(dòng)流程圖如下
app啟動(dòng)的起始點(diǎn)
-
在前文的demo中纵柿,在
load
方法處加一個(gè)斷點(diǎn)
蜈抓,通過bt
堆棧信息查看app啟動(dòng)是從哪里開始的
【app啟動(dòng)起點(diǎn)】:通過程序運(yùn)行發(fā)現(xiàn),是從
dyld
中的_dyld_start
開始的昂儒,所以需要去OpenSource下載一份dyld的源碼來進(jìn)行分析
也可以通過xcode左側(cè)的堆棧信息來找到入口
dyld::_main函數(shù)源碼分析
-
在
dyld-750.6
源碼中查找_dyld_start
,查找arm64架構(gòu)
發(fā)現(xiàn)沟使,是由匯編實(shí)現(xiàn),通過匯編注釋發(fā)現(xiàn)會(huì)調(diào)用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
方法渊跋,是一個(gè)C++
方法(以arm64架構(gòu)為例)
源碼中搜索dyldbootstrap
找到命名作用空間
腊嗡,再在這個(gè)文件中查找start
方法,其核心是返回值的調(diào)用了dyld
的main
函數(shù)拾酝,其中macho_header
是Mach-O
的頭部燕少,而dyld
加載的文件就是Mach-O類型
的,即Mach-O類型是可執(zhí)行文件類型
蒿囤,由四部分組成:Mach-O頭部客们、Load Command、section、Other Data
,可以通過MachOView
查看可執(zhí)行文件信息
進(jìn)入dyld::_main
的源碼實(shí)現(xiàn),特別長(zhǎng)待牵,大約600多行,如果對(duì)dyld加載流程不太了解的童鞋凤优,可以根據(jù)_main
函數(shù)的返回值進(jìn)行反推,這里就多作說明。在_main函數(shù)中主要做了一下幾件事情:
-
【第一步:
環(huán)境變量配置
】:根據(jù)環(huán)境變量設(shè)置相應(yīng)的值以及獲取當(dāng)前運(yùn)行架構(gòu) 【第二步:
共享緩存
】:檢查是否開啟了共享緩存,以及共享緩存是否映射到共享區(qū)域沸手,例如UIKit
、CoreFoundation
等
- 【第三步:
主程序的初始化
】:調(diào)用instantiateFromLoadedImage
函數(shù)實(shí)例化了一個(gè)ImageLoader
對(duì)象
- 【第四步:
插入動(dòng)態(tài)庫
】:遍歷DYLD_INSERT_LIBRARIES
環(huán)境變量注簿,調(diào)用loadInsertedDylib
加載
- 【第五步:
link 主程序
】
- 【第六步:
link 動(dòng)態(tài)庫
】
- 【第七步:
弱符號(hào)綁定
】
- 【第八步:
執(zhí)行初始化方法
】
【第九步:尋找主程序入口
即main
函數(shù)】:從Load Command
讀取LC_MAIN
入口契吉,如果沒有,就讀取LC_UNIXTHREAD
滩援,這樣就來到了日常開發(fā)中熟悉的main
函數(shù)了
第三步:主程序初始化
-
sMainExecutable
表示主程序變量,查看其賦值塔嬉,是通過instantiateFromLoadedImage
方法初始化
進(jìn)入instantiateFromLoadedImage
源碼玩徊,其中創(chuàng)建一個(gè)ImageLoader
實(shí)例對(duì)象,通過instantiateMainExecutable
方法創(chuàng)建
進(jìn)入instantiateMainExecutable
源碼谨究,其作用是為主可執(zhí)行文件創(chuàng)建映像恩袱,返回一個(gè)ImageLoader
類型的image對(duì)象,即主程序
胶哲。其中sniffLoadCommands
函數(shù)時(shí)獲取Mach-O類型文件
的Load Command
的相關(guān)信息畔塔,并對(duì)其進(jìn)行各種校驗(yàn)
第八步:執(zhí)行初始化方法
-
進(jìn)入
initializeMainExecutable
源碼,主要是循環(huán)遍歷
,都會(huì)執(zhí)行runInitializers
方法 runInitializers
其核心代碼是processInitializers
函數(shù)的調(diào)用進(jìn)入processInitializers
函數(shù)的源碼實(shí)現(xiàn)鸯屿,其中對(duì)鏡像列表調(diào)用recursiveInitialization
函數(shù)進(jìn)行遞歸實(shí)例化全局搜索
recursiveInitialization(
函數(shù),其源碼實(shí)現(xiàn)如下
在這里澈吨,需要分成兩部分探索,一部分是notifySingle
函數(shù)寄摆,一部分是doInitialization
函數(shù)谅辣,首先探索notifySingle
函數(shù)
notifySingle 函數(shù)
-
全局搜索
notifySingle(
函數(shù),其重點(diǎn)是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
這句
全局搜索sNotifyObjCInit
,發(fā)現(xiàn)沒有找到實(shí)現(xiàn)婶恼,有賦值操作
搜索registerObjCNotifiers
在哪里調(diào)用了桑阶,發(fā)現(xiàn)在_dyld_objc_notify_register
進(jìn)行了調(diào)用
注意:_dyld_objc_notify_register
的函數(shù)需要在libobjc
源碼中搜索
在objc4-781
源碼中搜索_dyld_objc_notify_register
,發(fā)現(xiàn)在_objc_init
源碼中調(diào)用了該方法勾邦,并傳入了參數(shù)蚣录,所以sNotifyObjCInit
的賦值
的就是objc
中的load_images
,而load_images
會(huì)調(diào)用所有的+load
方法眷篇。所以綜上所述萎河,notifySingle
是一個(gè)回調(diào)函數(shù)
load函數(shù)加載
下面我們進(jìn)入load_images
的源碼看看其實(shí)現(xiàn),以此來證明load_images
中調(diào)用了所有的load
函數(shù)
-
通過objc源碼中_objc_init源碼實(shí)現(xiàn),進(jìn)入
load_images
的源碼實(shí)現(xiàn)
進(jìn)入call_load_methods
源碼實(shí)現(xiàn)公壤,可以發(fā)現(xiàn)其核心是通過do-while
循環(huán)調(diào)用+load
方法
進(jìn)入call_class_loads
源碼實(shí)現(xiàn)换可,了解到這里調(diào)用的load
方法證實(shí)我們前文提及的類的load
方法
所以,load_images
調(diào)用了所有的load
函數(shù)厦幅,以上的源碼分析過程正好對(duì)應(yīng)堆棧的打印信息
【總結(jié)】load的源碼鏈為:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一個(gè)回調(diào)處理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
那么問題又來了沾鳄,_objc_init是什么時(shí)候調(diào)用的呢?請(qǐng)接著往下看
走到objc
的_objc_init
函數(shù)确憨,發(fā)現(xiàn)走不通了译荞,我們回退到recursiveInitialization
遞歸函數(shù)的源碼實(shí)現(xiàn),發(fā)現(xiàn)我們忽略了一個(gè)函數(shù)doInitialization
進(jìn)入doInitialization
函數(shù)的源碼實(shí)現(xiàn)
這里也需要分成兩部分休弃,一部分是doImageInit
函數(shù)吞歼,一部分是doModInitFunctions
函數(shù)
-
進(jìn)入
doImageInit
源碼實(shí)現(xiàn),其核心主要是for循環(huán)加載方法的調(diào)用
塔猾,這里需要注意的一點(diǎn)是篙骡,libSystem
的初始化必須先運(yùn)行
進(jìn)入doModInitFunctions
源碼實(shí)現(xiàn),這個(gè)方法中加載了所有C++
文件
可以通過測(cè)試程序的堆棧信息來驗(yàn)證,在C++方法處加一個(gè)斷點(diǎn)
走到這里丈甸,還是沒有找到_objc_init的調(diào)用糯俗?怎么辦呢?放棄嗎睦擂?當(dāng)然不行得湘,我們還可以通過_objc_init
加一個(gè)符號(hào)斷點(diǎn)來查看調(diào)用_objc_init前的堆棧信息,
-
_objc_init
加一個(gè)符號(hào)斷點(diǎn)顿仇,運(yùn)行程序淘正,查看_objc_init
斷住后的堆棧信息
在libsystem
中查找libSystem_initializer
,查看其中的實(shí)現(xiàn)
根據(jù)前面的堆棧信息臼闻,我們發(fā)現(xiàn)走的是libSystem_initializer
中會(huì)調(diào)用libdispatch_init
函數(shù)鸿吆,而這個(gè)函數(shù)的源碼是在libdispatch
開源庫中的,在libdispatch
中搜索libdispatch_init
進(jìn)入_os_object_init
源碼實(shí)現(xiàn)述呐,其源碼實(shí)現(xiàn)調(diào)用了_objc_init
函數(shù)
結(jié)合上面的分析伞剑,從初始化_objc_init
注冊(cè)的_dyld_objc_notify_register
的參數(shù)2,即load_images
市埋,到sNotifySingle
--> sNotifyObjCInie=參數(shù)2
到sNotifyObjcInit()
調(diào)用黎泣,形成了一個(gè)閉環(huán)
所以可以簡(jiǎn)單的理解為sNotifySingle
這里是添加通知即addObserver
,_objc_init
中調(diào)用_dyld_objc_notify_register
相當(dāng)于發(fā)送通知缤谎,即push
抒倚,而sNotifyObjcInit
相當(dāng)于通知的處理函數(shù),即selector
【總結(jié)】:_objc_init的源碼鏈:_dyld_start
--> dyldbootstrap::start
--> dyld::_main
--> dyld::initializeMainExecutable
--> ImageLoader::runInitializers
--> ImageLoader::processInitializers
--> ImageLoader::recursiveInitialization
--> doInitialization
-->libSystem_initializer
(li#### 第九步:尋找主入口函數(shù)
-
匯編調(diào)試坷澡,可以看到顯示來到
+[ViewController load]
方法[圖片上傳中...(image-5e2d0b-1613204313417-0)]
bSystem.B.dylib) -->_os_object_init
(libdispatch.dylib) -->_objc_init
(libobjc.A.dylib)
第九步:尋找主入口函數(shù)
-
匯編調(diào)試托呕,可以看到顯示來到
+[ViewController load]
方法
繼續(xù)執(zhí)行,來到kcFunc
的C++函數(shù)
點(diǎn)擊stepover
,繼續(xù)往下,跑完了整個(gè)流程项郊,會(huì)回到_dyld_start
,然后調(diào)用main()
函數(shù),通過匯編完成main
的參數(shù)賦值等操作
dyld
匯編源碼實(shí)現(xiàn)
注意:main
是寫定的函數(shù)馅扣,寫入內(nèi)存,讀取到dyld
着降,如果修改了main函數(shù)的名稱
差油,會(huì)報(bào)錯(cuò)
所以,綜上所述任洞,最終dyld加載流程
蓄喇,如下圖所示,圖中也詮釋了前文中的問題:為什么是load-->Cxx-->main
的調(diào)用順序