本文的目的主要是分析dyld
的加載流程囚巴,了解在main函數(shù)之前,底層還做了什么
引子
-
創(chuàng)建一個project氢卡,在
ViewController
中重寫了load
方法,在main
中加了一個C++
方法,即kcFUnc
,請問它們的打印先后順序是什么寓涨?
-
運行程序,查看 load氯檐、kcFunc戒良、main的
打印順序
,下面是打印結(jié)果冠摄,通過結(jié)果可以看出其順序是load --> C++方法 --> main
為什么是這么一個順序糯崎?按照常規(guī)的思維理解,main不是入口函數(shù)嗎河泳?為什么不是main最先執(zhí)行拇颅?
下面根據(jù)這個問題,我們來探索在走到main之前乔询,到底還做了什么。
編譯過程及庫
在分析app啟動之前韵洋,我們需要先了解iOSapp代碼的編譯過程
以及動態(tài)庫
和靜態(tài)庫
竿刁。
編譯過程
其中編譯過程
如下圖所示,主要分為以下幾步:
-
源文件
:載入.h搪缨、.m食拜、.cpp等文件 -
預(yù)處理
:替換宏,刪除注釋副编,展開頭文件负甸,產(chǎn)生.i文件 -
編譯
:將.i文件轉(zhuǎn)換為匯編語言,產(chǎn)生.s文件 -
匯編
:將匯編文件轉(zhuǎn)換為機(jī)器碼文件痹届,產(chǎn)生.o文件 -
鏈接
:對.o文件中引用其他庫的地方進(jìn)行引用呻待,生成最后的可執(zhí)行文件
靜態(tài)庫 和 動態(tài)庫
-
靜態(tài)庫
:在鏈接階段,會將可匯編生成的目標(biāo)程序與引用的庫一起鏈接打包到可執(zhí)行文件當(dāng)中队腐。此時的靜態(tài)庫就不會在改變了蚕捉,因為它是編譯時被直接拷貝一份,復(fù)制到目標(biāo)程序里的
好處
:編譯完成后柴淘,庫文件實際上就沒有作用了迫淹,目標(biāo)程序沒有外部依賴秘通,直接就可以運行缺點
:由于靜態(tài)庫會有兩份,所以會導(dǎo)致目標(biāo)程序的體積增大
敛熬,對內(nèi)存肺稀、性能、速度消耗很大
-
動態(tài)庫
:程序編譯時并不會鏈接到目標(biāo)程序
中应民,目標(biāo)程序只會存儲指向動態(tài)庫的引用话原,在程序運行時才被載入
-
優(yōu)勢
:減少打包之后app的大小
:因為不需要拷貝至目標(biāo)程序中,所以不會影響目標(biāo)程序的體積瑞妇,與靜態(tài)庫相比稿静,減少了app的體積大小共享內(nèi)存,節(jié)約資源
:同一份庫可以被多個程序使用通過
更新動態(tài)庫辕狰,達(dá)到更新程序
的目的:由于運行時才載入的特性改备,可以隨時對庫進(jìn)行替換,而不需要重新編譯代碼
-
缺點
:動態(tài)載入會帶來一部分性能損失
蔓倍,使用動態(tài)庫也會使得程序依賴于外部環(huán)境悬钳,如果環(huán)境缺少了動態(tài)庫,或者庫的版本不正確偶翅,就會導(dǎo)致程序無法運行
-
靜態(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負(fù)責(zé)連接,加載程序
所以 App的啟動流程圖如下
app啟動的起始點
-
在前文的demo中朵耕,在
load
方法處加一個斷點
炫隶,通過bt
堆棧信息查看app啟動是從哪里開始的
【app啟動起點】:通過程序運行發(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)伪阶,是由匯編實現(xiàn),通過匯編注釋發(fā)現(xiàn)會調(diào)用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
方法处嫌,是一個C++
方法(以arm64架構(gòu)為例)
-
源碼中搜索
dyldbootstrap
找到命名作用空間
栅贴,再在這個文件中查找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
的源碼實現(xiàn)陶衅,特別長,大約600多行直晨,如果對dyld加載流程不太了解的童鞋搀军,可以根據(jù)_main
函數(shù)的返回值進(jìn)行反推,這里就多作說明勇皇。在_main函數(shù)中主要做了一下幾件事情:-
【第一步:
環(huán)境變量配置
】:根據(jù)環(huán)境變量設(shè)置相應(yīng)的值以及獲取當(dāng)前運行架構(gòu)
-
【第二步:
共享緩存
】:檢查是否開啟了共享緩存罩句,以及共享緩存是否映射到共享區(qū)域,例如UIKit
敛摘、CoreFoundation
等
-
【第三步:
主程序的初始化
】:調(diào)用instantiateFromLoadedImage
函數(shù)實例化了一個ImageLoader
對象
-
【第四步:
插入動態(tài)庫
】:遍歷DYLD_INSERT_LIBRARIES
環(huán)境變量门烂,調(diào)用loadInsertedDylib
加載
-
【第五步:
link 主程序
】
-
【第六步:
link 動態(tài)庫
】
-
【第七步:
弱符號綁定
】
-
【第八步:
執(zhí)行初始化方法
】
-
【第九步:
尋找主程序入口
即main
函數(shù)】:從Load Command
讀取LC_MAIN
入口,如果沒有兄淫,就讀取LC_UNIXTHREAD
屯远,這樣就來到了日常開發(fā)中熟悉的main
函數(shù)了
-
下面主要分析下【第三步】和【第八步】
第三步:主程序初始化
-
sMainExecutable
表示主程序變量,查看其賦值捕虽,是通過instantiateFromLoadedImage
方法初始化
-
進(jìn)入
instantiateFromLoadedImage
源碼慨丐,其中創(chuàng)建一個ImageLoader
實例對象,通過instantiateMainExecutable
方法創(chuàng)建
-
進(jìn)入
instantiateMainExecutable
源碼泄私,其作用是為主可執(zhí)行文件創(chuàng)建映像房揭,返回一個ImageLoader
類型的image對象,即主程序
晌端。其中sniffLoadCommands
函數(shù)時獲取Mach-O類型文件
的Load Command
的相關(guān)信息捅暴,并對其進(jìn)行各種校驗
第八步:執(zhí)行初始化方法
-
進(jìn)入
initializeMainExecutable
源碼,主要是循環(huán)遍歷
,都會執(zhí)行runInitializers
方法
-
全局搜索
runInitializers(cons
,找到如下源碼斩松,其核心代碼是processInitializers
函數(shù)的調(diào)用
-
進(jìn)入
processInitializers
函數(shù)的源碼實現(xiàn),其中對鏡像列表調(diào)用recursiveInitialization
函數(shù)進(jìn)行遞歸實例化
-
全局搜索
recursiveInitialization(cons
函數(shù),其源碼實現(xiàn)如下
在這里觉既,需要分成兩部分探索惧盹,一部分是notifySingle
函數(shù),一部分是doInitialization
函數(shù)瞪讼,首先探索notifySingle
函數(shù)
notifySingle 函數(shù)
-
全局搜索
notifySingle(
函數(shù),其重點是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
這句
-
全局搜索
sNotifyObjCInit
钧椰,發(fā)現(xiàn)沒有找到實現(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
會調(diào)用所有的+load
方法养筒。所以綜上所述,notifySingle
是一個回調(diào)函數(shù)
load函數(shù)加載
下面我們進(jìn)入load_images
的源碼看看其實現(xiàn)端姚,以此來證明load_images
中調(diào)用了所有的load
函數(shù)
-
通過objc源碼中_objc_init源碼實現(xiàn)晕粪,進(jìn)入
load_images
的源碼實現(xiàn)
-
進(jìn)入
call_load_methods
源碼實現(xiàn),可以發(fā)現(xiàn)其核心是通過do-while
循環(huán)調(diào)用+load
方法
-
進(jìn)入
call_class_loads
源碼實現(xiàn)渐裸,了解到這里調(diào)用的load
方法證實我們前文提及的類的load
方法
所以巫湘,load_images
調(diào)用了所有的load
函數(shù),以上的源碼分析過程正好對應(yīng)堆棧的打印信息
【總結(jié)】load的源碼鏈為:
_dyld_start
--> dyldbootstrap::start
--> dyld::_main
--> dyld::initializeMainExecutable
--> ImageLoader::runInitializers
--> ImageLoader::processInitializers
--> ImageLoader::recursiveInitialization
--> dyld::notifySingle
(是一個回調(diào)處理) --> sNotifyObjCInit
--> load_images(libobjc.A.dylib)
那么問題又來了昏鹃,_objc_init是什么時候調(diào)用的呢尚氛?請接著往下看
doInitialization 函數(shù)
-
走到
objc
的_objc_init
函數(shù)成艘,發(fā)現(xiàn)走不通了戏阅,我們回退到recursiveInitialization
遞歸函數(shù)的源碼實現(xiàn),發(fā)現(xiàn)我們忽略了一個函數(shù)doInitialization
-
進(jìn)入
doInitialization
函數(shù)的源碼實現(xiàn)
這里也需要分成兩部分闸婴,一部分是doImageInit
函數(shù)您宪,一部分是doModInitFunctions
函數(shù)- 進(jìn)入
doImageInit
源碼實現(xiàn)奈懒,其核心主要是for循環(huán)加載方法的調(diào)用
,這里需要注意的一點是宪巨,libSystem
的初始化必須先運行
- 進(jìn)入
doModInitFunctions
源碼實現(xiàn)磷杏,這個方法中加載了所有Cxx
文件
可以通過測試程序的堆棧信息來驗證,在C++方法處加一個斷點
- 進(jìn)入
走到這里,還是沒有找到_objc_init的調(diào)用捏卓?怎么辦呢极祸?放棄嗎?當(dāng)然不行怠晴,我們還可以通過_objc_init
加一個符號斷點來查看調(diào)用_objc_init前的堆棧信息遥金,
-
_objc_init
加一個符號斷點,運行程序蒜田,查看_objc_init
斷住后的堆棧信息
-
在
libsystem
中查找libSystem_initializer
稿械,查看其中的實現(xiàn)
-
根據(jù)前面的堆棧信息,我們發(fā)現(xiàn)走的是
libSystem_initializer
中會調(diào)用libdispatch_init
函數(shù)冲粤,而這個函數(shù)的源碼是在libdispatch
開源庫中的美莫,在libdispatch
中搜索libdispatch_init
-
進(jìn)入
_os_object_init
源碼實現(xiàn),其源碼實現(xiàn)調(diào)用了_objc_init
函數(shù)
結(jié)合上面的分析梯捕,從初始化_objc_init
注冊的_dyld_objc_notify_register
的參數(shù)2厢呵,即load_images
,到sNotifySingle
-->sNotifyObjCInie=參數(shù)2
到sNotifyObjcInit()
調(diào)用傀顾,形成了一個閉環(huá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
(libSystem.B.dylib) --> _os_object_init
(libdispatch.dylib) --> _objc_init
(libobjc.A.dylib)
第九步:尋找主入口函數(shù)
-
匯編調(diào)試赐劣,可以看到顯示來到
+[ViewController load]
方法
-
繼續(xù)執(zhí)行,來到
kcFunc
的C++函數(shù)
-
點擊
stepover
,繼續(xù)往下入撒,跑完了整個流程隆豹,會回到_dyld_start
,然后調(diào)用main()
函數(shù),通過匯編完成main
的參數(shù)賦值等操作
dyld
匯編源碼實現(xiàn)
注意:
main
是寫定的函數(shù),寫入內(nèi)存茅逮,讀取到dyld
璃赡,如果修改了main函數(shù)的名稱
,會報錯
所以献雅,綜上所述碉考,最終dyld加載流程
,如下圖所示挺身,圖中也詮釋了前文中的問題:為什么是load-->Cxx-->main
的調(diào)用順序