iOS 應(yīng)用程序加載流程分析

本文的目的主要是分析dyld的加載流程

首先我們先運(yùn)行個(gè)代碼 來引入我們今天的主題~~


運(yùn)行結(jié)果:

運(yùn)行程序,查看 load、kcFunc、main的打印順序,下面是打印結(jié)果,通過結(jié)果可以看出其順序是?load --> C++方法 --> main

為什么是這么一個(gè)順序?

編譯過程及庫(kù)

在分析app啟動(dòng)之前,我們需要先了解iOSapp代碼的編譯過程以及動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)。

編譯過程

其中編譯過程如下圖所示,主要分為以下幾步:

源文件:載入.h、.m、.cpp等文件

預(yù)處理:替換宏,刪除注釋,展開頭文件,產(chǎn)生.i文件

編譯:將.i文件轉(zhuǎn)換為匯編語(yǔ)言,產(chǎn)生.s文件

匯編:將匯編文件轉(zhuǎn)換為機(jī)器碼文件,產(chǎn)生.o文件

鏈接:對(duì).o文件中引用其他庫(kù)的地方進(jìn)行引用,生成最后的可執(zhí)行文件


靜態(tài)庫(kù) 和 動(dòng)態(tài)庫(kù)

靜態(tài)庫(kù):在鏈接階段,會(huì)將可匯編生成的目標(biāo)程序與引用的庫(kù)一起鏈接打包到可執(zhí)行文件當(dāng)中。此時(shí)的靜態(tài)庫(kù)就不會(huì)在改變了,因?yàn)樗蔷幾g時(shí)被直接拷貝一份,復(fù)制到目標(biāo)程序里的

好處:編譯完成后,庫(kù)文件實(shí)際上就沒有作用了,目標(biāo)程序沒有外部依賴,直接就可以運(yùn)行

缺點(diǎn):由于靜態(tài)庫(kù)會(huì)有兩份,所以會(huì)導(dǎo)致目標(biāo)程序的體積增大,對(duì)內(nèi)存、性能、速度消耗很大

動(dòng)態(tài)庫(kù):程序編譯時(shí)并不會(huì)鏈接到目標(biāo)程序中,目標(biāo)程序只會(huì)存儲(chǔ)指向動(dòng)態(tài)庫(kù)的引用,在程序運(yùn)行時(shí)才被載入

優(yōu)勢(shì):

減少打包之后app的大小:因?yàn)椴恍枰截愔聊繕?biāo)程序中,所以不會(huì)影響目標(biāo)程序的體積,與靜態(tài)庫(kù)相比,減少了app的體積大小

共享內(nèi)存,節(jié)約資源:同一份庫(kù)可以被多個(gè)程序使用

通過更新動(dòng)態(tài)庫(kù),達(dá)到更新程序的目的:由于運(yùn)行時(shí)才載入的特性,可以隨時(shí)對(duì)庫(kù)進(jìn)行替換,而不需要重新編譯代碼

缺點(diǎn):動(dòng)態(tài)載入會(huì)帶來一部分性能損失,使用動(dòng)態(tài)庫(kù)也會(huì)使得程序依賴于外部環(huán)境,如果環(huán)境缺少了動(dòng)態(tài)庫(kù),或者庫(kù)的版本不正確,就會(huì)導(dǎo)致程序無法運(yùn)行



?dyld 是英文 the dynamic link editor 的簡(jiǎn)寫,翻譯過來就是動(dòng)態(tài)鏈接器,是蘋果操作系統(tǒng)的一個(gè)重要的組成部分。在 iOS/Mac OSX 系統(tǒng)中,僅有很少量的進(jìn)程只需要內(nèi)核就能完成加載,基本上所有的進(jìn)程都是動(dòng)態(tài)鏈接的,所以 Mach-O 鏡像文件中會(huì)有很多對(duì)外部的庫(kù)和符號(hào)的引用,但是這些引用并不能直接用,在啟動(dòng)時(shí)還必須要通過這些引用進(jìn)行內(nèi)容的填補(bǔ),這個(gè)填補(bǔ)工作就是由 動(dòng)態(tài)鏈接器dyld 來完成的,也就是符號(hào)綁定。動(dòng)態(tài)鏈接器dyld 在系統(tǒng)中以一個(gè)用戶態(tài)的可執(zhí)行文件形式存在,一般應(yīng)用程序會(huì)在 Mach-O 文件部分指定一個(gè) LC_LOAD_DYLINKER 的加載命令,此加載命令指定了 dyld 的路徑,通常它的默認(rèn)值是 /usr/lib/dyld 。系統(tǒng)內(nèi)核在加載 Mach-O 文件時(shí),都需要用 dyld(位于 /usr/lib/dyld )程序進(jìn)行鏈接。

dyld加載流程分析

首先把load方法加個(gè)斷點(diǎn) bt堆棧信息

1.找程序入口:_dyld_start?

然后我們就可以去源碼中找一下?下面就以?x86_64?為例來分析一下?dyld。

我們可以借助注釋來了解其流程。其實(shí)在我們LLDB?調(diào)試堆棧的時(shí)候已經(jīng)看到??_dyld_start?函數(shù)之后走的是?dyldbootstrap::start?函數(shù)。源碼中搜索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ù)中主要做了一下幾件事情:

1.環(huán)境變量配置:根據(jù)環(huán)境變量設(shè)置相應(yīng)的值以及獲取當(dāng)前運(yùn)行架構(gòu)


2.共享緩存:檢查是否開啟了共享緩存,以及共享緩存是否映射到共享區(qū)域,例如UIKit、CoreFoundation等


3.主程序初始化:調(diào)用instantiateFromLoadedImage函數(shù)實(shí)例化了一個(gè)ImageLoader對(duì)象

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ù)是獲取Mach-O類型文件的Load Command的相關(guān)信息,并對(duì)其進(jìn)行各種校驗(yàn)

4.插入動(dòng)態(tài)庫(kù):遍歷DYLD_INSERT_LIBRARIES環(huán)境變量,調(diào)用loadInsertedDylib加載

5.link 主程序

6.?link?動(dòng)態(tài)庫(kù)

7.弱符號(hào)綁定

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

進(jìn)入initializeMainExecutable源碼,主要是循環(huán)遍歷,都會(huì)執(zhí)行runInitializers方法

全局搜索runInitializers(cons,找到如下源碼,其核心代碼是processInitializers函數(shù)的調(diào)用

進(jìn)入processInitializers函數(shù)的源碼實(shí)現(xiàn),其中對(duì)鏡像列表調(diào)用recursiveInitialization函數(shù)進(jìn)行遞歸實(shí)例化

全局搜索recursiveInitialization(cons函數(shù),其源碼實(shí)現(xiàn)如下

在這里,需要分成兩部分探索,一部分是notifySingle函數(shù),一部分是doInitialization函數(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ù)加載

通過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ù)

那么問題又來了,_objc_init是什么時(shí)候調(diào)用的呢?

doInitialization 函數(shù)

recursiveInitialization遞歸函數(shù)的源碼實(shí)現(xiàn),發(fā)現(xiàn)我們忽略了一個(gè)函數(shù)doInitialization

進(jìn)入doInitialization函數(shù)的源碼實(shí)現(xiàn)

這里也需要分成兩部分,一部分是doImageInit函數(shù),一部分是doModInitFunctions函數(shù)

doImageInit

進(jìn)入doImageInit源碼實(shí)現(xiàn),其核心主要是for循環(huán)加載方法的調(diào)用,這里需要注意的一點(diǎn)是,libSystem的初始化必須先運(yùn)行

進(jìn)入doModInitFunctions源碼實(shí)現(xiàn),這個(gè)方法中加載了所有Cxx文件

通過測(cè)試程序的堆棧信息來驗(yàn)證,在C++方法處加一個(gè)斷點(diǎn)?

還是沒有找到_objc_init的調(diào)用?我們可以通過_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開源庫(kù)中的,在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é)】:_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)



9.尋找主程序入口即main函數(shù) :從Load Command讀取LC_MAIN入口,如果沒有,就讀取LC_UNIXTHREAD,這樣就來到了日常開發(fā)中熟悉的main函數(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加載流程,

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。