一個iOS app的main()函數位於main.m中,這是咱們熟知的程序入口。但對objc瞭解更多以後發現,程序在進入咱們的main函數前已經執行了不少代碼,好比熟知的+ load方法等。本文將跟隨程序執行順序,刨根問底,從dyld到runtime,看看main函數以前都發生了什麼。 html
iOS中用到的全部系統framework都是動態連接的,類比成插頭和插排,靜態連接的代碼在編譯後的靜態連接過程就將插頭和插排一個個插好,運行時直接執行二進制文件;而動態連接須要在程序啓動時去完成「插插銷」的過程,因此在咱們寫的代碼執行前,動態鏈接器須要完成準備工做。 git
這個是在xcode中看到的Link列表:
這些framework將會在動態連接過程當中被加載,另外還有隱含link的framework,能夠測試出來:先找到可執行文件,我這裏叫TestMain的工程,模擬器路徑下找到TestMain.app,可執行文件默認同名,再經過otool命令: github
1 |
$ otool -L TestMain |
-L參數打印出全部link的framework(去掉了版本信息): bootstrap
1 2 3 4 5 6 7 |
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 |
除了多了的CoreGraphics(被UIKit依賴)外,有兩個默認添加的lib。libobjc即objc和runtime,libSystem中包含了不少系統級別lib,列幾個熟知的:libdispatch(GCD),libsystem_c(C語言庫),libsystem_blocks(Block),libcommonCrypto(經常使用的md5函數)等等。這些lib都是dylib格式(如windows中的dll),系統使用動態連接有幾點好處: windows
dyld - the dynamic link editor(這縮寫對應的很奇怪,我感受是DYnamic Linker Daemon呢- -?)apple的動態連接器,系統kernel作好啓動程序的初始準備後,交給dyld負責,援引並翻譯《mikeask這篇blog》對dyld做用順序的歸納: xcode
得益於dyld是開源的,github地址,咱們能夠從源碼一探究竟。 緩存
一切源於dyldStartup.s這個文件,其中用匯編實現了名爲__dyld_start的方法,彙編太生澀,它主要乾了兩件事: app
這個步驟隨手就能驗證出來,設置一個符號斷點斷在_objc_init:
這個函數是runtime的初始化函數,後面會提到。程序運行在很早的時候斷住,這時候看調用棧:
看到了棧底的dyldbootstrap::start()方法,繼而調用了dyld::_main()方法,其中完成了剛纔說的遞歸加載動態庫過程,因爲libSystem默認引入,棧中出現了libSystem_initializer的初始化方法。 函數
固然這個image不是圖片的意思,它大概表示一個二進制文件(可執行文件或so文件),裏面是被編譯過的符號、代碼等,因此ImageLoader做用是將這些文件加載進內存,且每個文件對應一個ImageLoader實例來負責加載。
兩步走: 測試
固然全部這些都發生在咱們真正的main函數執行前。
剛纔講到libSystem是若干個系統lib的集合,因此它只是一個容器lib而已,並且它也是開源的,裏面實質上就一個文件,init.c,細節不說了,由libSystem_initializer逐步調用到了_objc_init,這裏就是objc和runtime的初始化入口。
除了runtime環境的初始化外,_objc_init中綁定了新image被加載後的callback:
1 2 3 |
dyld_register_image_state_change_handler(dyld_image_state_bound, 1/*batch*/, &map_images); dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images); |
可見dyld擔當了runtime和ImageLoader中間的協調者,當新image加載進來後交由runtime大廚去解析這個二進制文件的符號表和代碼。繼續上面的斷點法,斷住神祕的+load函數:
清楚的看到整個調用棧和順序:
至此,可執行文件中和動態庫全部的符號(Class,Protocol,Selector,IMP,…)都已經按格式成功加載到內存中,被runtime所管理,再這以後,runtime的那些方法(動態添加Class、方法混合等等才能生效)
Q: 重載本身Class的load方法時需不須要調父類?
A: runtime負責按繼承順序遞歸調用,因此咱們不能調super
Q: 在本身Class的load方法時能不能替換系統framework(好比UIKit)中的某個類的方法實現
A: 能夠,由於動態連接過程當中,全部依賴庫的類是先於本身的類加載的
Q: 重載load時須要手動添加@autoreleasepool麼?
A: 不須要,在runtime調用load方法先後是加了objc_autoreleasePoolPush()和objc_autoreleasePoolPop()的。
Q: 想讓一個類的load方法被調用是否須要在某個地方import這個文件
A: 不須要,只要這個類的符號被編譯到最後的可執行文件中,load方法就會被調用(Reveal SDK就是利用這一點,只要引入到工程中就能工做)
整個事件由dyld主導,完成運行環境的初始化後,配合ImageLoader將二進制文件按格式加載到內存,
動態連接依賴庫,並由runtime負責加載成objc定義的結構,全部初始化工做結束後,dyld調用真正的main函數。
值得說明的是,這個過程遠比寫出來的要複雜,這裏只提到了runtime這個分支,還有像GCD、XPC等重頭的系統庫初始化分支沒有說起(固然,有緩存機制在,它們也不會玩命初始化),總結起來就是main函數執行以前,系統作了茫茫多的加載和初始化工做,但都被很好的隱藏了,咱們無需關心。
當這一切都結束時,dyld會清理現場,將調用棧迴歸,只剩下:
孤獨的main函數,看上去是程序的開始,確是一段精彩的終結