iOS開發(fā)中霸琴,main函數(shù)是我們熟知的程序啟動入口,但實際上并非真正意義上的入口昭伸,因為在我們運行程序梧乘,再到main方法被調用之間,程序已經(jīng)做了許許多多的事情庐杨,比如我們熟知的runtime的初始化就發(fā)生在main函數(shù)調用前选调,還有程序動態(tài)庫的加載鏈接也發(fā)生在這階段,本文主要對從程序啟動到main函數(shù)中發(fā)生的主要事情進行簡單介紹灵份。
其實簡單總結起來就是:
系統(tǒng)先讀取App的可執(zhí)行文件(Mach-O文件)仁堪,從里面獲得dyld的路徑,然后加載dyld填渠,dyld去初始化運行環(huán)境弦聂,開啟緩存策略,加載程序相關依賴庫(其中也包含我們的可執(zhí)行文件)氛什,并對這些庫進行鏈接莺葫,最后調用每個依賴庫的初始化方法,在這一步枪眉,runtime被初始化捺檬。當所有依賴庫的初始化后,輪到最后一位(程序可執(zhí)行文件)進行初始化贸铜,在這時runtime會對項目中所有類進行類結構初始化堡纬,然后調用所有的load方法聂受。最后dyld返回main函數(shù)地址,main函數(shù)被調用烤镐,我們便來到了熟悉的程序入口饺饭。
下面我們將結合代碼對整個過程進行分析:
dyld加載
這里先說下Mach-O文件。
Mach-O文件格式是 OS X 與 iOS 系統(tǒng)上的可執(zhí)行文件格式职车,像我們編譯過程產(chǎn)生的.O文件瘫俊,以及程序的可執(zhí)行文件,動態(tài)庫等都是Mach-O文件悴灵。它的結構如下:
有如下幾個部分組成:
- Header:保存了一些基本信息扛芽,包括了該文件運行的平臺、文件類型积瞒、LoadCommands的個數(shù)等等川尖。
- LoadCommands:可以理解為加載命令,在加載Mach-O文件時會使用這里的數(shù)據(jù)來確定內(nèi)存的分布以及相關的加載命令茫孔。比如我們的main函數(shù)的加載地址叮喳,程序所需的dyld的文件路徑,以及相關依賴庫的文件路徑缰贝。
- Data: 這里包含了具體的代碼馍悟、數(shù)據(jù)等等。
我們可以通過Mach-O文件查看器MachOView查看一個測試項目(這里放上地址)編譯后的可執(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了退敦。
從dyld開始
dyld: (the dynamic link editor)動態(tài)鏈接器歧斟,其源碼是開源的拣技。
ImageLoader: 用于輔助加載特定可執(zhí)行文件格式的類疹味,程序中對應實例可簡稱為image(如程序可執(zhí)行文件,F(xiàn)ramework庫房交,bundle文件)彻舰。
dyld接手后得做很多事情,主要負責初始化程序環(huán)境,將可執(zhí)行文件以及相應的依賴庫與插入庫加載進內(nèi)存生成對應的ImageLoader類的image(鏡像文件)對象刃唤,對這些image進行鏈接隔心,調用各image的初始化方法等等(注:這里多數(shù)事情都是遞歸的,從底向上的方法調用)尚胞,其中runtime也是在這個過程中被初始化硬霍,這些事情大多數(shù)在dyld:_mian方法中被發(fā)生,我們可以看段簡潔的代碼:
這里的_main函數(shù)是dyld的函數(shù)笼裳,并非我們程序里的main函數(shù)唯卖。
1.sMainExecutable = instantiateFromLoadedImage(....)與loadInsertedDylib(...)
這一步dyld將我們可執(zhí)行文件以及插入的lib加載進內(nèi)存,生成對應的image。
sMainExecutable對應著我們的可執(zhí)行文件躬柬,里面包含了我們項目中所有新建的類拜轨。
InsertDylib一些插入的庫,他們配置在全局的環(huán)境變量sEnv中允青,我們可以在項目中設置環(huán)境變量DYLD_PRINT_ENV為1來打印該sEnv的值橄碾。
運行程序Log如下:
可以看到插入的庫為:libBacktraceRecording.dylib和libViewDebuggerSupport.
有時我們會在三方App的Mach-O文件中通過修改DYLD_INSERT_LIBRARIES的值來加入我們自己的動態(tài)庫,從而注入代碼颠锉,hook別人的App(相關資料)法牲。
2.link(sMainExecutable,...)和link(image,....)
對上面生成的Image進行進行鏈接琼掠。其主要做的事有對image進行l(wèi)oad(加載),rebase(基地址復位)拒垃,bind(外部符號綁定),我們可以查看源碼:
recursiveLoadLibraries(context, preflightOnly, loaderRPaths)
遞歸加載所有依賴庫進內(nèi)存眉枕。recursiveRebase(context)
遞歸對自己以及依賴庫進行復基位操作恶复。在以前怜森,程序每次加載其在內(nèi)存中的堆椝偬簦基地址都是一樣的,這意味著你的方法副硅,變量等地址每次都一樣的姥宝,這使得程序很不安全,后面就出現(xiàn)ASLR(Address space layout randomization,地址空間配置隨機加載)恐疲,程序每次啟動后地址都會隨機變化腊满,這樣程序里所有的代碼地址都是錯的,需要重新對代碼地址進行計算修復才能正常訪問培己。recursiveBind(context, forceLazysBound, neverUnload)
對庫中所有nolazy的符號進行bind,一般的情況下多數(shù)符號都是lazybind的碳蛋,他們在第一次使用的時候才進行bind。
3.initializeMainExecutable()
這一步主要是調用所有image的Initalizer方法進行初始化省咨。這里的Initalizers方法并非名為Initalizers的方法肃弟,而是C++靜態(tài)對象初始化構造器,atribute((constructor))進行修飾的方法,在LmageLoader類中initializer函數(shù)指針所指向該初始化方法的地址笤受。
我們可以在程序中設置環(huán)境變量DYLD_PRINT_INITIALIZERS為1來打印出程序的各種依賴庫的initializer方法:
運行程序穷缤,系統(tǒng)Log打印如下:
(由于打印的比較長,這樣就截取開頭的log)可以看到每個依賴庫對應著一個初始化方法箩兽,名稱各有不同津肛。
這里最開始調用的libSystem.dylib的initializer function比較特殊,因為runtime初始化就在這一階段汗贫,而這個方法其實很簡單身坐,我們可以在這里看到init.c源碼,主要方法如下:
其中l(wèi)ibdispatch_init里調用了到了runtime初始化方法_objc_init.我們可以落包、在程序中打個符號斷點來驗證:
運行程序掀亥,然后斷點命中,我們來看下調用棧:
這里可以看到_objc_init調用的順序妥色,先libSystem_initializer調用libdispatch_init再到_objc_init初始化runtime搪花。
runtime初始化后不會閑著,在_objc_init中注冊了幾個通知嘹害,從dyld這里接手了幾個活撮竿,其中包括負責初始化相應依賴庫里的類結構,調用依賴庫里所有的laod方法笔呀。
就拿sMainExcuatable來說幢踏,它的initializer方法是最后調用的,當initializer方法被調用前dyld會通知runtime進行類結構初始化许师,然后再通知調用load方法房蝉,這些目前還發(fā)生在main函數(shù)前,但由于lazy bind機制微渠,依賴庫多數(shù)都是在使用時才進行bind搭幻,所以這些依賴庫的類結構初始化都是發(fā)生在程序里第一次使用到該依賴庫時才進行的。
main函數(shù)被調用
當所有的依賴庫庫的lnitializer都調用完后逞盆,dyld::main函數(shù)會返回程序的main函數(shù)地址檀蹋,main函數(shù)被調用,從而代碼來到了我們熟悉的程序入口云芦。
結語
這里只是簡單了概括了從程序啟動->dyld加載依賴庫->runtime初始化->main 的過程俯逾。但這階段還有很多事情未講,如果想深入了解還得結合源碼來學習舅逸,這里我已經(jīng)將dyld和runtime源碼都放在這了桌肴,大家可直接下載,也可以從opensource-apple下載琉历。
再嘮嗑會
dyld源碼前前后后讀個大概懂坠七,花了我3個多禮拜的空閑時間,由于C和C++基礎并不是很好,所以特意跑回學校買了幾本書補了下基礎灼捂,不過讀源碼的這段時間還是挺累的离例。
為什么要去讀源碼,主要是看別人的文章時并不能很好解決我的某些疑問悉稠,而且只有真正去認識源碼宫蛆,去親身體會才能加深對它的理解。
學習的旅途雖然頗累的猛,但一路下來收獲頗多耀盗。加油!
前行路卦尊,路漫漫叛拷,一人一酒似逍遙。
參考資料
1.Mach-O 可執(zhí)行文件
2.dylib動態(tài)庫加載過程分析
3.iOS 程序 main 函數(shù)之前發(fā)生了什么
4.今日頭條iOS客戶端啟動速度優(yōu)化
5.App 啟動時間:過去岂却,現(xiàn)在和未來
6.優(yōu)化 App 的啟動時間
7.dyld在hook方面的小東西
喜歡的話點個喜歡唄_