主要參考:
iOS程序啟動(dòng)->dyld加載->runtime初始化 過程
iOS 程序 main 函數(shù)之前發(fā)生了什么
一個(gè)iOS App
的 main
函數(shù)位于main.m
中豁辉,這是我們熟知的程序入口。但對objc
了解更多之后發(fā)現(xiàn)持钉,程序在進(jìn)入我們的main函數(shù)前已經(jīng)執(zhí)行了很多代碼,比如熟知的+load
方法等肄渗。
簡單總結(jié)
系統(tǒng)先讀取
App
的可執(zhí)行文件(Mach-O
文件)琅绅,從里面獲得dyld
的路徑,然后加載dyld
,dyld
去初始化運(yùn)行環(huán)境眠屎。開啟緩存策略,加載程序相關(guān)依賴庫(其中也包含我們的可執(zhí)行文件)肆饶,并對這些庫進(jìn)行鏈接改衩,最后調(diào)用每個(gè)依賴庫的初始化方法,在這一步驯镊,
runtime
被初始化葫督。當(dāng)所有依賴庫的初始化后,輪到最后一位(程序可執(zhí)行文件)進(jìn)行初始化板惑,在這時(shí)
runtime
會對項(xiàng)目中所有類進(jìn)行類機(jī)構(gòu)初始化橄镜,然后調(diào)用所有的load
方法。最后dyld
返回main
函數(shù)地址冯乘,main
函數(shù)被調(diào)用洽胶,我們便來到程序入口main函數(shù)。
一. 從dyld開始
Mach-O文件
Mach-O
文件格式是OS X
與iOS
系統(tǒng)上的可執(zhí)行文件格式裆馒,像我們編譯過程產(chǎn)生的.O
文件姊氓,以及程序的可執(zhí)行文件,動(dòng)態(tài)庫等都是Mach-O
文件喷好,它的結(jié)構(gòu)如下:
-
Header
: 保存了一些基本信息翔横,包括了該文件運(yùn)行的平臺、文件類型梗搅、LoadCommands
的個(gè)數(shù)等禾唁。
-LoadCommands
: 可以理解為加載命令,在加載Mach-O
文件時(shí)會使用這里的數(shù)據(jù)來確定內(nèi)存的分布以及相關(guān)的加載命令无切。比如我們的main
函數(shù)的加載地址弊予,程序所需的dyld
的文件路徑黔漂,以及相關(guān)依賴庫的文件路徑张漂。
-Data
:這里包含了具體的代碼冕杠、數(shù)據(jù)等矛洞。
我們可以通過Mach-O
文件查看器MachOView
查看一個(gè)項(xiàng)目編譯后的可執(zhí)行文件內(nèi)容:
可以看出:
-
dyld
的路徑在LC_LOAD_DYLINKER
命令里洼哎,一般都是在/usr/lib/dyld
路徑下烫映。 -
LC_MAIN
指的是程序main
函數(shù)加載地址 -
LC_LOAD_DYLIB
指向的都是程序依賴庫加載信息。 - 如果我們程序使用到
AFNetworking
噩峦,這里就會多出一條名LC_LOAD_DYLIB(AFNetworking)
的命令锭沟。如下圖:
可以看出我們比較常用的三方庫: AFNetworking
,IQKeyboard
等。
系統(tǒng)加載程序可執(zhí)行文件后识补,通過分析文件來獲得dyld
所在路徑來加載dyld
族淮,然后就將后面的事情交給dyld
.
動(dòng)態(tài)鏈接庫
iOS
中用到的所有系統(tǒng)framework
都是動(dòng)態(tài)鏈接的,類比成插頭和插排凭涂,靜態(tài)鏈接的代碼在編譯后的靜態(tài)鏈接過程就將插頭和插排一個(gè)個(gè)插好祝辣,運(yùn)行時(shí)直接執(zhí)行二進(jìn)制文件;而動(dòng)態(tài)鏈接需要在程序啟動(dòng)時(shí)有需要再去完成插好相關(guān)的插頭和插排切油,所以在我們寫的代碼執(zhí)行前蝙斜,動(dòng)態(tài)連接器需要完成準(zhǔn)備工作。
這個(gè)是在Xcode
中看到的Link
列表:
這些framework
將會在動(dòng)態(tài)連接過程中被加載澎胡,另外還有隱含link的framework,可以測試出來:先找到可執(zhí)行文件孕荠,我這里叫TestMain
的工程,模擬器路徑下找到TestMain.app
,可執(zhí)行文件默認(rèn)同名攻谁,在通過otool
命令:
$ otool -L TestMain
-L
參數(shù)打印出所有link
的framework
(去掉了版本信息如下)
TestMain:
/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics
/System/Library/Frameworks/UIKit.framework/UIKit
/System/Library/Frameworks/Foundation.framework/Foundation
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
/usr/lib/libobjc.A.dylib
/usr/lib/libSystem.dylib
出了多了CoreFoundation
(被UIKit依賴)外稚伍,有兩個(gè)默認(rèn)添加的lib: libobjc
即objc
和runtime
, libSystem
中包含了很多系統(tǒng)級別的lib
戚宦,列幾個(gè)熟知的个曙。
- libdispatch(GCD)
- libsystem_c(C語言庫)
- libsystem_blocks(Block)
- libCommonCrypto(加密庫,比如常用的md5)
這些lib
都是dylib
格式相當(dāng)于windows
中的dll
阁苞,系統(tǒng)使用動(dòng)態(tài)鏈接好處:
代碼共用: 很多程序都動(dòng)態(tài)鏈接了這些
lib
困檩,但是它們在內(nèi)存和磁盤中只有一份易于維護(hù):由于被依賴的
lib
是程序執(zhí)行時(shí)才link
的,所以這些lib
很容易做更新那槽,比如libSystem.dylib
是libSystem.B.dylib
的替身悼沿,哪天想升級直接換成libSystem.C.dylib
然后再替換替身就可以減少可執(zhí)行文件體積,相比靜態(tài)鏈接骚灸,動(dòng)態(tài)鏈接在編譯時(shí)不需要打包進(jìn)去糟趾,所以可執(zhí)行文件的體積要小很多。
dyld
dyld(the dynamic link editor)
, Apple
的動(dòng)態(tài)鏈接器甚牲,系統(tǒng)kernel
做好啟動(dòng)程序的初始準(zhǔn)備后义郑,交給dyld
負(fù)責(zé),dyld
作用順序的概括:
1. 從kernel留下的原始調(diào)用棧引導(dǎo)和啟動(dòng)自己
2. 將程序依賴的動(dòng)態(tài)鏈接庫遞歸加載進(jìn)內(nèi)存丈钙,當(dāng)然這里有緩存機(jī)制
3.non-lazy符號立即link到可執(zhí)行文件非驮,lazy的存表里
4.Runs static initializers for the executable
5. 找到可執(zhí)行文件的main函數(shù),準(zhǔn)備參數(shù)并調(diào)用
6. 程序執(zhí)行中負(fù)責(zé)綁定lazy符號雏赦、提供runtime dynamic loading services劫笙、提供調(diào)試器接口芙扎。
7. 程序main函數(shù)return后執(zhí)行static terminator
8. 某些場景下main函數(shù)結(jié)束后調(diào)libSystem的_exit函數(shù)。
由于dyld
是開源的填大,我們可以看到dyldStartup.s
這個(gè)文件戒洼,其中用匯編實(shí)現(xiàn)名為_dyld_start
的方法,匯編太生澀允华,它主要做了這件事:
1. 調(diào)用dyldbootstrap::start()方法(省去參數(shù))
2.上一個(gè)方法返回了main函數(shù)地址圈浇,填入?yún)?shù)并調(diào)用main函數(shù)。
這個(gè)步驟可以通過設(shè)置一個(gè)符號斷點(diǎn)斷在_objc_init
:
這個(gè)函數(shù)是runtime
的初始化函數(shù)靴寂。程序運(yùn)行在很早的時(shí)候斷住磷蜀,這時(shí)候看調(diào)用棧:
看到棧底的dyldbootstrap::start()
方法,繼而調(diào)用了dyld::_main()
方法榨汤,其中完成了剛從說的遞歸加載動(dòng)態(tài)庫過程蠕搜,由于libSystem
默認(rèn)引入,棧中出現(xiàn)了libSystem_initializer
的初始化方法收壕。
我們可以看下_main
函數(shù):
這里的_main
函數(shù)是dyld
的函數(shù)妓灌,并非我們程序里的main
函數(shù)。
1. sMainExecutable = instantiateFromLoadedImage(....)與loadInsertedDylib(...)
這一步 dyld
將我們可執(zhí)行文件以及插入的lib
加載進(jìn)內(nèi)存蜜宪,生成對應(yīng)的image
.
sMainExecutable
對應(yīng)著我們的可執(zhí)行文件虫埂,里面包含了我們項(xiàng)目中所有新建的類。
insertDylib
一些插入的庫圃验,他們配置在全局的環(huán)境變量sEnv
中掉伏,我們可以在項(xiàng)目中設(shè)置環(huán)境變量DYLD_PRINT_ENV
為1
,來打印該sEnv
的值澳窑。
運(yùn)行log
如下:
可以看出插入的庫為:libBacktraceRecording.dylib
和libViewDebuggerSupport
.
有時(shí)我們會在三方App
的Mach-O
文件中通過修改DYLD_INSERT_LIBRARIES
的值來加入我們自己的動(dòng)態(tài)庫斧散,從而注入代碼,hook
別人的App
.
2. link(sMainExecutable,...)
和 link(image, ...)
對上面生成的image
進(jìn)行鏈接摊聋。其主要有對image
進(jìn)行load(加載)
鸡捐、rebase(基地址復(fù)位)
,bind(外部符號綁定)
,我們可以查看源碼:
-
recursiveLoadLibraries(context, prefightOnly,loaderRPaths)
遞歸加載所有依賴庫進(jìn)內(nèi)存
-recursiveRebase(context)
遞歸對自己以及依賴庫進(jìn)行復(fù)基位操作麻裁。在以前箍镜,程序每次加載其在內(nèi)存中的堆棧地址都是一樣的,這意味著你的方法煎源,變量等地址每次都一樣的色迂,這使得程序很不安全,后面就出現(xiàn)ASLR(Address space layout randomization
,地址空間配置隨機(jī)加載)手销,程序每次啟動(dòng)后地址都會隨機(jī)變化歇僧,這樣程序里所有的代碼地址都是錯(cuò),需要重新對代碼地址進(jìn)行計(jì)算修復(fù)才能正常訪問锋拖。
-
recursiveBind(context, forceLazyBound诈悍,neverUnload)
對庫中所有nolazy
的符號進(jìn)行bind
埂淮,一般情況下多數(shù)符號都是lazybind
的,他們在第一次使用的時(shí)候才進(jìn)行bind
.
3.initializeMainExecutable()
這一步主要是調(diào)用所有image
的initalizer
方法進(jìn)行初始化写隶。這里的initalizers
方法并非名為Initalizers
的方法,而是C++靜態(tài)對象初始化構(gòu)造器讲仰,atribute(constructor)
進(jìn)行修飾的方法慕趴,在LmageLoader
類中initializer
函數(shù)指針鎖指向該初始化方法的地址。
我們可以在程序中設(shè)置環(huán)境變量DYLD_PRINT_INITALIZERS
為1
來打印出程序的各種依賴庫的initializer方法鄙陡。
運(yùn)行程序冕房,系統(tǒng)log
打印如下:
可以看到每個(gè)依賴庫對應(yīng)著一個(gè)初始化方法,名稱各有不同趁矾。
這里最開始調(diào)用的libSystem.dylib
的initializer function
比較特殊耙册,因?yàn)?code>runtime初始化就在這一階段,而這個(gè)方法其實(shí)和簡單毫捣,我們可以在這里看到init.c
源碼详拙,主要方法如下:
其中libdispatch_init
里調(diào)用了到runtime
初始化方法_objc_init.
我們可以在程序中打個(gè)符號斷點(diǎn)來驗(yàn)證。
運(yùn)行程序蔓同,然后斷點(diǎn)命中饶辙,我們來看下調(diào)用棧:
我們可以看到_objc_init
調(diào)用順序,先libSystem_initializer
調(diào)用libdispatch_init
斑粱,再到_objc_init
初始化runtime
.
runtime
初始化后不會閑著弃揽,在_objc_init
中注冊了幾個(gè)同志,從dyld
這里接手幾個(gè)活则北,其中包括初始化相應(yīng)依賴庫里的類結(jié)構(gòu)矿微,調(diào)用依賴庫里所有load
方法。
就拿sMainExcuateable
來說尚揣,它的initializer
方法是最后調(diào)用的涌矢,當(dāng)initializer
方法被調(diào)用前dyld
會通知runtime
進(jìn)行類結(jié)構(gòu)初始化,然后再通知調(diào)用+load
方法惑艇,這些目前都發(fā)生在main
函數(shù)前蒿辙,但是由于lazy bind
機(jī)制,依賴庫多數(shù)都是在使用時(shí)才進(jìn)行bind
滨巴,所以這些依賴庫的類結(jié)構(gòu)初始化都是發(fā)生在程序里第一次使用到該依賴庫時(shí)才進(jìn)行思灌。
ImageLoader
當(dāng)然這個(gè)image
不是圖片的意思,它大概表示一個(gè)二進(jìn)制文件(可執(zhí)行文件或so文件)恭取,里面是被編譯過的符號泰偿、代碼等,所以imageLoader
作用是將這些文件加載進(jìn)內(nèi)存蜈垮,且每一個(gè)文件對應(yīng)一個(gè)imageLoader
實(shí)例來負(fù)責(zé)加載耗跛。
兩步走:
1.在程序運(yùn)行時(shí)它先將動(dòng)態(tài)鏈接的image遞歸加載(也就是上面ImageLoader的遞歸調(diào)用)
2.再從可執(zhí)行文件image遞歸加載所有符號
當(dāng)然所有這些都發(fā)生在我們真正的main函數(shù)執(zhí)行之前裕照。
runtime 與 +load
剛才講到libSystem
是若干個(gè)系統(tǒng)lib
的集合,所以它只是一個(gè)容器lib
而已调塌,而且它也是開源的晋南,里面實(shí)質(zhì)上就是一個(gè)文件: init.c 由libSystem_initializer逐步調(diào)用到了_objc_init
,這里就是objc
和runtime
的初始化入口羔砾。
除了runtime
環(huán)境的初始化外负间,_objc_init
中綁定了新image
被加載后的callback
:
dyld_register_image_state_change_handler(
dyld_image_state_bound, 1, &map_images);
dyld_register_image_state_change_handler(
dyld_image_state_dependents_initialized, 0, &load_images);
可見dyld
擔(dān)當(dāng)了runtime
和imageLoader
中間的協(xié)調(diào)者,當(dāng)新image
加載進(jìn)來后交由runtime
去解析這個(gè)二進(jìn)制文件的符號表和代碼姜凄。繼續(xù)上面的斷點(diǎn)法政溃,斷住神秘的+load
函數(shù)。
清楚的看到整個(gè)調(diào)用棧和順序:
1. dyld開始將程序二進(jìn)制文件初始化
2. 交由imageLoader讀取image态秧,其中包含了我們的類董虱,方法等各種符號
3.由于runtime向dyld綁定了回調(diào),當(dāng)image加載到內(nèi)存后申鱼,dyld會通知runtime進(jìn)行處理
4. runtime接手后調(diào)用map_images做解析和處理愤诱,接下來load_images中調(diào)用call_load_methods方法,遍歷所有加載進(jìn)來的Class润讥,按繼承層級依次調(diào)用Class的+load方法和Category的+load方法转锈。
至此,可執(zhí)行文件中和動(dòng)態(tài)庫所有的符號(Class, Protocol,Selector,IMP,...
)都已經(jīng)按格式成功加載到內(nèi)存中楚殿,被runtime
所管理撮慨,再這之后,runtime
的那些方法(動(dòng)態(tài)添加Class脆粥,swizzie
等等才能生效)
關(guān)于+load方法的幾個(gè)QA
Q:重載自己Class
的+load
方法需不需要調(diào)父類
A:runtime
負(fù)責(zé)按繼承順序遞歸調(diào)用砌溺,所以我們不能調(diào)用super
Q: 在自己Class
的+load
方法時(shí)能不能替換系統(tǒng)framework
(比如UIKit
)中某個(gè)類的方法實(shí)現(xiàn)
A:可以,因?yàn)樵趧?dòng)態(tài)鏈接過程中变隔,所有依賴庫的類是優(yōu)先于自己的類加載的
Q:重載+load
時(shí)需要手動(dòng)添加@autoreleasepool
嗎规伐?
A:不需要,在runtime
調(diào)用+load
方法前后是加了objc_autoreleasePoolPush()
和objc_autoreleasePoolPop()
的匣缘。
Q:想讓一個(gè)類的+load
方法被調(diào)用是否需要在某個(gè)地方import
這個(gè)文件
A:不需要猖闪,只要這個(gè)類的符號被編譯到最后的可執(zhí)行文件中,+load
方法就會被調(diào)用.
總結(jié)
整個(gè)事件由
dyld
主導(dǎo)肌厨,完成運(yùn)行環(huán)境的初始化后培慌,配合ImageLoader
將二進(jìn)制文件按格式加載到內(nèi)存動(dòng)態(tài)鏈接依賴庫,并由
runtime
負(fù)責(zé)加載成objc
定義的結(jié)構(gòu)柑爸,所有初始化工作結(jié)束后吵护,dyld
調(diào)用真正的main
函數(shù)。值得說明的是,這個(gè)過程遠(yuǎn)比寫出來復(fù)雜馅而,這里只提到了
runtime
這個(gè)分支祥诽,還有像GCD、XPC
瓮恭、等重頭的系統(tǒng)庫初始化分支沒有提及(當(dāng)然這里還有緩存機(jī)制)總結(jié):在main函數(shù)執(zhí)行之前雄坪,系統(tǒng)做了茫茫多的加載和初始化工作,但是被很好隱藏了屯蹦。
孤獨(dú)的main函數(shù)
當(dāng)所有前期初始化工作結(jié)束是诸衔,dyld會清理現(xiàn)場,將調(diào)用椘溺瑁回歸,只剩下:
孤獨(dú)的main
函數(shù)就缆,看上去像是程序的開始帖渠!