main()調用之前的加載過程
App開始啟動后,系統首先加載可執行文件(自身App的所有.o文件的集合),然后加載動態鏈接庫dyld。
dyld是一個專門用來加載動態鏈接庫的庫。
dyld源碼鏈接
執行從dyld開始,dyld從可執行文件的依賴開始, 遞歸加載所有的依賴動態鏈接庫。
動態鏈接庫包括:
iOS 中用到的所有系統 framework
加載OC runtime方法的libobjc,
系統級別的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。
其實無論對于系統的動態鏈接庫還是對于App本身的可執行文件而言,他們都算是image(鏡像),而每個App都是以image(鏡像)為單位進行加載的。
什么是image(鏡像)
1.executable可執行文件 比如.o文件。
2.dylib 動態鏈接庫 framework就是動態鏈接庫和相應資源包含在一起的一個文件夾結構。
3.bundle 資源文件 只能用dlopen加載,不推薦使用這種方式加載。
除了我們App本身的可行性文件,系統中所有的framework比如UIKit、Foundation等都是以動態鏈接庫的方式集成進App中的。
系統使用動態鏈接有幾點好處:
代碼共用:很多程序都動態鏈接了這些 lib,但它們在內存和磁盤中中只有一份。 易于維護:由于被依賴的 lib 是程序執行時才鏈接的,所以這些 lib 很容易做更新,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升級直接換成libSystem.C.dylib 然后再替換替身就行了。 減少可執行文件體積:相比靜態鏈接,動態鏈接在編譯時不需要打進去,所以可執行文件的體積要小很多。
如上圖所示,不同進程之間共用系統dylib的_TEXT區,但是各自維護對應的_DATA區。
所有動態鏈接庫和我們App中的靜態庫.a和所有類文件編譯后的.o文件最終都是由dyld(the dynamic link editor),Apple的動態鏈接器來加載到內存中。每個image都是由一個叫做ImageLoader的類來負責加載(一一對應),那么ImageLoader又是什么呢?
什么是ImageLoader
image 表示一個二進制文件(可執行文件或 so 文件),里面是被編譯過的符號、代碼等,所以 ImageLoader 作用是將這些文件加載進內存,且每一個文件對應一個ImageLoader實例來負責加載。
兩步走: 在程序運行時它先將動態鏈接的 image 遞歸加載 (也就是上面測試棧中一串的遞歸調用的時刻)。 再從可執行文件 image 遞歸加載所有符號。
當然所有這些都發生在我們真正的main函數執行前。
動態鏈接庫加載的具體流程
動態鏈接庫的加載步驟具體分為5步:
1.load dylibs image 讀取庫鏡像文件
2.Rebase image
3.Bind image
4.Objc setup
5.initializers
1.load dylibs image
在每個動態庫的加載過程中, dyld需要:
1.分析所依賴的動態庫
2.找到動態庫的mach-o文件
3.打開文件
4.驗證文件
5.在系統核心注冊文件簽名
6.對動態庫的每一個segment調用mmap()
通常的,一個App需要加載100到400個dylibs, 但是其中的系統庫被優化,可以很快的加載。
針對這一步驟的優化有:
1.減少非系統庫的依賴
2.合并非系統庫
3.使用靜態資源,比如把代碼加入主程序
2.rebase/bind
由于ASLR(address space layout randomization)的存在,可執行文件和動態鏈接庫在虛擬內存中的加載地址每次啟動都不固定,所以需要這2步來修復鏡像中的資源指針,來指向正確的地址。
rebase修復的是指向當前鏡像內部的資源指針;
而bind指向的是鏡像外部的資源指針。
rebase步驟先進行,需要把鏡像讀入內存,并以page為單位進行加密驗證,保證不會被篡改,所以這一步的瓶頸在IO。
bind在其后進行,由于要查詢符號表,來指向跨鏡像的資源,加上在rebase階段,鏡像已被讀入和加密驗證,所以這一步的瓶頸在于CPU計算。
//通過命令行可以查看相關的資源指針:
xcrun dyldinfo -rebase -bind -lazy_bind myApp.App/myApp
優化該階段的關鍵在于減少__DATA segment中的指針數量。
可以優化的點有:
1.減少Objc類數量, 減少selector數量
2.減少C++虛函數數量
3.轉而使用swift stuct(其實本質上就是為了減少符號的數量)
3.Objc setup
這一步主要工作是:
1.注冊Objc類 (class registration)
2.把category的定義插入方法列表 (category registration)
3.保證每一個selector唯一 (selctor uniquing)
4.由于之前2步驟的優化,這一步實際上沒有什么可做的。
4.initializers
以上三步屬于靜態調整(fix-up),都是在修改__DATA segment中的內容,而這里則開始動態調整,開始在堆和堆棧中寫入內容。 在這里的工作有:
1.Objc的+load()函數
2.C++的構造函數屬性函數 形如attribute((constructor)) void DoSomeInitializationWork()
3.非基本類型的C++靜態全局變量的創建(通常是類或結構體)(non-trivial initializer) 比如一個全局靜態結構體的構建,如果在構造函數中有繁重的工作,那么會拖慢啟動速度
Objc的load函數和C++的靜態構造函數采用由底向上的方式執行,來保證每個執行的方法,都可以找到所依賴的動態庫。
1).dyld 開始將程序二進制文件初始化
2).交由 ImageLoader 讀取 image,其中包含了我們的類、方法等各種符號
3).由于 runtime 向 dyld 綁定了回調,當 image 加載到內存后,dyld 會通知 runtime 進行處理
4).runtime 接手后調用 mapimages 做解析和處理,接下來 loadimages 中調用 callloadmethods 方法,遍歷所有加載進來的 Class,按繼承層級依次調用 Class 的 +load 方法和其 Category 的 +load 方法
至此
至此,可執行文件中和動態庫所有的符號(Class,Protocol,Selector,IMP,…)都已經按格式成功加載到內存中,被 runtime 所管理,再這之后,runtime 的那些方法(動態添加 Class、swizzle 等等才能生效)。
整個事件由 dyld 主導,完成運行環境的初始化后,配合 ImageLoader 將二進制文件按格式加載到內存, 動態鏈接依賴庫,并由 runtime 負責加載成 objc 定義的結構,所有初始化工作結束后,dyld 調用真正的 main 函數。