iOS 程序 main函數(shù)之前發(fā)生什么

主要參考:
iOS程序啟動->dyld加載->runtime初始化 過程
iOS 程序 main 函數(shù)之前發(fā)生了什么

image.png

一個iOS Appmain函數(shù)位于main.m中,這是我們熟知的程序入口。但對objc了解更多之后發(fā)現(xiàn),程序在進入我們的main函數(shù)前已經(jīng)執(zhí)行了很多代碼,比如熟知的+load方法等。

簡單總結(jié)

  • 系統(tǒng)先讀取App的可執(zhí)行文件(Mach-O文件),從里面獲得dyld的路徑,然后加載dyld,dyld去初始化運行環(huán)境。

  • 開啟緩存策略,加載程序相關依賴庫(其中也包含我們的可執(zhí)行文件),并對這些庫進行鏈接,最后調(diào)用每個依賴庫的初始化方法,在這一步,runtime被初始化。

  • 當所有依賴庫的初始化后,輪到最后一位(程序可執(zhí)行文件)進行初始化,在這時runtime會對項目中所有類進行類機構(gòu)初始化,然后調(diào)用所有的load方法。最后dyld返回main函數(shù)地址,main函數(shù)被調(diào)用,我們便來到程序入口main函數(shù)。

一. 從dyld開始

Mach-O文件

Mach-O文件格式是OS XiOS系統(tǒng)上的可執(zhí)行文件格式,像我們編譯過程產(chǎn)生的.O文件,以及程序的可執(zhí)行文件,動態(tài)庫等都是Mach-O文件,它的結(jié)構(gòu)如下:

image.png
  • Header: 保存了一些基本信息,包括了該文件運行的平臺、文件類型、LoadCommands的個數(shù)等。

-LoadCommands: 可以理解為加載命令,在加載Mach-O文件時會使用這里的數(shù)據(jù)來確定內(nèi)存的分布以及相關的加載命令。比如我們的main函數(shù)的加載地址,程序所需的dyld的文件路徑,以及相關依賴庫的文件路徑。

-Data:這里包含了具體的代碼、數(shù)據(jù)等。

我們可以通過Mach-O文件查看器MachOView查看一個項目編譯后的可執(zhí)行文件內(nèi)容:

Mach-O文件內(nèi)容.png

可以看出:

  • dyld的路徑在LC_LOAD_DYLINKER命令里,一般都是在/usr/lib/dyld路徑下。
  • LC_MAIN指的是程序main函數(shù)加載地址
  • LC_LOAD_DYLIB指向的都是程序依賴庫加載信息。
  • 如果我們程序使用到AFNetworking,這里就會多出一條名LC_LOAD_DYLIB(AFNetworking)的命令。如下圖:
三方庫.png

可以看出我們比較常用的三方庫: AFNetworking,IQKeyboard等。

系統(tǒng)加載程序可執(zhí)行文件后,通過分析文件來獲得dyld所在路徑來加載dyld,然后就將后面的事情交給dyld.

動態(tài)鏈接庫

iOS 中用到的所有系統(tǒng)framework都是動態(tài)鏈接的,類比成插頭和插排,靜態(tài)鏈接的代碼在編譯后的靜態(tài)鏈接過程就將插頭和插排一個個插好,運行時直接執(zhí)行二進制文件;而動態(tài)鏈接需要在程序啟動時有需要再去完成插好相關的插頭和插排,所以在我們寫的代碼執(zhí)行前,動態(tài)連接器需要完成準備工作。

這個是在Xcode中看到的Link列表:

image.png

這些framework將會在動態(tài)連接過程中被加載,另外還有隱含link的framework,可以測試出來:先找到可執(zhí)行文件,我這里叫TestMain的工程,模擬器路徑下找到TestMain.app,可執(zhí)行文件默認同名,在通過otool命令:

$ otool -L TestMain

-L參數(shù)打印出所有linkframework(去掉了版本信息如下)

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依賴)外,有兩個默認添加的lib: libobjcobjcruntime, libSystem中包含了很多系統(tǒng)級別的lib,列幾個熟知的。

- libdispatch(GCD)
- libsystem_c(C語言庫)
- libsystem_blocks(Block)
- libCommonCrypto(加密庫,比如常用的md5)

這些lib都是dylib格式相當于windows中的dll,系統(tǒng)使用動態(tài)鏈接好處:

  • 代碼共用: 很多程序都動態(tài)鏈接了這些lib,但是它們在內(nèi)存和磁盤中只有一份

  • 易于維護:由于被依賴的lib是程序執(zhí)行時才link的,所以這些lib很容易做更新,比如libSystem.dyliblibSystem.B.dylib的替身,哪天想升級直接換成libSystem.C.dylib然后再替換替身就可以

  • 減少可執(zhí)行文件體積,相比靜態(tài)鏈接,動態(tài)鏈接在編譯時不需要打包進去,所以可執(zhí)行文件的體積要小很多。

dyld

dyld(the dynamic link editor), Apple 的動態(tài)鏈接器,系統(tǒng)kernel做好啟動程序的初始準備后,交給dyld負責,dyld作用順序的概括:

1. 從kernel留下的原始調(diào)用棧引導和啟動自己
2. 將程序依賴的動態(tài)鏈接庫遞歸加載進內(nèi)存,當然這里有緩存機制
3.non-lazy符號立即link到可執(zhí)行文件,lazy的存表里
4.Runs static initializers for the executable
5. 找到可執(zhí)行文件的main函數(shù),準備參數(shù)并調(diào)用
6. 程序執(zhí)行中負責綁定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這個文件,其中用匯編實現(xiàn)名為_dyld_start的方法,匯編太生澀,它主要做了這件事:

1. 調(diào)用dyldbootstrap::start()方法(省去參數(shù))
2.上一個方法返回了main函數(shù)地址,填入?yún)?shù)并調(diào)用main函數(shù)。

這個步驟可以通過設置一個符號斷點斷在_objc_init

image.png

這個函數(shù)是runtime的初始化函數(shù)。程序運行在很早的時候斷住,這時候看調(diào)用棧:

image.png

看到棧底的dyldbootstrap::start()方法,繼而調(diào)用了dyld::_main()方法,其中完成了剛從說的遞歸加載動態(tài)庫過程,由于libSystem默認引入,棧中出現(xiàn)了libSystem_initializer的初始化方法。

我們可以看下_main函數(shù):

dyld::_main函數(shù)代碼.png

這里的_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_ENV1,來打印該sEnv的值。

環(huán)境變量設置.png

運行log如下:

插入庫log.png

可以看出插入的庫為:libBacktraceRecording.dyliblibViewDebuggerSupport.

有時我們會在三方AppMach-O文件中通過修改DYLD_INSERT_LIBRARIES的值來加入我們自己的動態(tài)庫,從而注入代碼,hook別人的App.

2. link(sMainExecutable,...)link(image, ...)
對上面生成的image進行鏈接。其主要有對image進行load(加載)、rebase(基地址復位),bind(外部符號綁定),我們可以查看源碼:

link方法.png
  • recursiveLoadLibraries(context, prefightOnly,loaderRPaths)
    遞歸加載所有依賴庫進內(nèi)存

-recursiveRebase(context)
遞歸對自己以及依賴庫進行復基位操作。在以前,程序每次加載其在內(nèi)存中的堆棧地址都是一樣的,這意味著你的方法,變量等地址每次都一樣的,這使得程序很不安全,后面就出現(xiàn)ASLR(Address space layout randomization,地址空間配置隨機加載),程序每次啟動后地址都會隨機變化,這樣程序里所有的代碼地址都是錯,需要重新對代碼地址進行計算修復才能正常訪問。

  • recursiveBind(context, forceLazyBound,neverUnload)
    對庫中所有nolazy的符號進行bind,一般情況下多數(shù)符號都是lazybind的,他們在第一次使用的時候才進行bind.

3.initializeMainExecutable()
這一步主要是調(diào)用所有imageinitalizer方法進行初始化。這里的initalizers方法并非名為Initalizers的方法,而是C++靜態(tài)對象初始化構(gòu)造器,atribute(constructor)進行修飾的方法,在LmageLoader類中initializer函數(shù)指針鎖指向該初始化方法的地址。

initallizer函數(shù)指針.png

我們可以在程序中設置環(huán)境變量DYLD_PRINT_INITALIZERS1來打印出程序的各種依賴庫的initializer方法。

image.png

運行程序,系統(tǒng)log打印如下:

Initializer調(diào)用log.png

可以看到每個依賴庫對應著一個初始化方法,名稱各有不同。

這里最開始調(diào)用的libSystem.dylibinitializer function比較特殊,因為runtime初始化就在這一階段,而這個方法其實和簡單,我們可以在這里看到init.c源碼,主要方法如下:

libSystem_initializer.png

其中libdispatch_init里調(diào)用了到runtime初始化方法_objc_init.我們可以在程序中打個符號斷點來驗證。

_objc_init.png

運行程序,然后斷點命中,我們來看下調(diào)用棧:


objc_init調(diào)用棧.png

我們可以看到_objc_init調(diào)用順序,先libSystem_initializer調(diào)用libdispatch_init,再到_objc_init初始化runtime.

runtime初始化后不會閑著,在_objc_init中注冊了幾個同志,從dyld這里接手幾個活,其中包括初始化相應依賴庫里的類結(jié)構(gòu),調(diào)用依賴庫里所有load方法。

就拿sMainExcuateable來說,它的initializer方法是最后調(diào)用的,當initializer方法被調(diào)用前dyld會通知runtime進行類結(jié)構(gòu)初始化,然后再通知調(diào)用+load方法,這些目前都發(fā)生在main函數(shù)前,但是由于lazy bind機制,依賴庫多數(shù)都是在使用時才進行bind,所以這些依賴庫的類結(jié)構(gòu)初始化都是發(fā)生在程序里第一次使用到該依賴庫時才進行。

ImageLoader

當然這個image不是圖片的意思,它大概表示一個二進制文件(可執(zhí)行文件或so文件),里面是被編譯過的符號、代碼等,所以imageLoader作用是將這些文件加載進內(nèi)存,且每一個文件對應一個imageLoader實例來負責加載。

兩步走:

1.在程序運行時它先將動態(tài)鏈接的image遞歸加載(也就是上面ImageLoader的遞歸調(diào)用)
2.再從可執(zhí)行文件image遞歸加載所有符號

當然所有這些都發(fā)生在我們真正的main函數(shù)執(zhí)行之前。

runtime 與 +load

剛才講到libSystem是若干個系統(tǒng)lib的集合,所以它只是一個容器lib而已,而且它也是開源的,里面實質(zhì)上就是一個文件: init.c 由libSystem_initializer逐步調(diào)用到了_objc_init,這里就是objcruntime的初始化入口。

除了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擔當了runtimeimageLoader中間的協(xié)調(diào)者,當新image加載進來后交由runtime去解析這個二進制文件的符號表和代碼。繼續(xù)上面的斷點法,斷住神秘的+load函數(shù)。

image.png

清楚的看到整個調(diào)用棧和順序:

1. dyld開始將程序二進制文件初始化

2. 交由imageLoader讀取image,其中包含了我們的類,方法等各種符號

3.由于runtime向dyld綁定了回調(diào),當image加載到內(nèi)存后,dyld會通知runtime進行處理

4. runtime接手后調(diào)用map_images做解析和處理,接下來load_images中調(diào)用call_load_methods方法,遍歷所有加載進來的Class,按繼承層級依次調(diào)用Class的+load方法和Category的+load方法。

至此,可執(zhí)行文件中和動態(tài)庫所有的符號(Class, Protocol,Selector,IMP,...)都已經(jīng)按格式成功加載到內(nèi)存中,被runtime所管理,再這之后,runtime的那些方法(動態(tài)添加Class,swizzie等等才能生效)

關于+load方法的幾個QA

Q:重載自己Class+load方法需不需要調(diào)父類
A:runtime負責按繼承順序遞歸調(diào)用,所以我們不能調(diào)用super

Q: 在自己Class+load方法時能不能替換系統(tǒng)framework(比如UIKit)中某個類的方法實現(xiàn)
A:可以,因為在動態(tài)鏈接過程中,所有依賴庫的類是優(yōu)先于自己的類加載的

Q:重載+load時需要手動添加@autoreleasepool嗎?
A:不需要,在runtime調(diào)用+load方法前后是加了objc_autoreleasePoolPush()objc_autoreleasePoolPop()的。

Q:想讓一個類的+load方法被調(diào)用是否需要在某個地方import這個文件
A:不需要,只要這個類的符號被編譯到最后的可執(zhí)行文件中,+load方法就會被調(diào)用.

總結(jié)

  • 整個事件由dyld主導,完成運行環(huán)境的初始化后,配合ImageLoader將二進制文件按格式加載到內(nèi)存

  • 動態(tài)鏈接依賴庫,并由runtime負責加載成objc定義的結(jié)構(gòu),所有初始化工作結(jié)束后,dyld調(diào)用真正的main函數(shù)。

  • 值得說明的是,這個過程遠比寫出來復雜,這里只提到了runtime這個分支,還有像GCD、XPC、等重頭的系統(tǒng)庫初始化分支沒有提及(當然這里還有緩存機制)

  • 總結(jié):在main函數(shù)執(zhí)行之前,系統(tǒng)做了茫茫多的加載和初始化工作,但是被很好隱藏了。

孤獨的main函數(shù)

當所有前期初始化工作結(jié)束是,dyld會清理現(xiàn)場,將調(diào)用?;貧w,只剩下:

image.png

孤獨的main函數(shù),看上去像是程序的開始!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容