iOS程序main函數之前發生了什麽
我是前言
一個iOS app的main()
函數位於main.m
中,這是我們熟知的程序入口。但對objc了解更多之後發現,程序在進入我們的main
函數前已經執行了很多代碼,比如熟知的+ load
方法等。本文將跟隨程序執行順序,刨根問底,從dyld
到runtime
,看看main函數之前都發生了什麽。
從dyld開始
動態鏈接庫
iOS中用到的所有系統framework都是動態鏈接的,類比成插頭和插排,靜態鏈接的代碼在編譯後的靜態鏈接過程就將插頭和插排一個個插好,運行時直接執行二進制文件;而動態鏈接需要在程序啟動時去完成“插插銷”的過程,所以在我們寫的代碼執行前,動態連接器需要完成準備工作。
這個是在xcode中看到的Link列表:
這些framework將會在動態鏈接過程中被加載,另外還有隱含link的framework,可以測試出來:先找到可執行文件,我這裏叫TestMain
的工程,模擬器路徑下找到TestMain.app
,可執行文件默認同名,再通過otool
命令:
$ otool -L TestMain
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
-L參數打印出所有link的framework(去掉了版本信息):
除了多了的CoreGraphics
(被UIKit依賴)外,有兩個默認添加的lib。libobjc即objc和runtime,libSystem中包含了很多系統級別lib,列幾個熟知的:libdispatch(GCD),libsystem_c(C語言庫),libsystem_blocks(Block),libcommonCrypto(常用的md5函數)等等。這些lib都是dylib
格式(如windows中的dll),系統使用動態鏈接有幾點好處:
- 代碼共用:很多程序都動態鏈接了這些lib,但它們在內存和磁盤中中只有一份
- 易於維護:由於被依賴的lib是程序執行時才link的,所以這些lib很容易做更新,比如
libSystem.dylib
libSystem.B.dylib
的替身,哪天想升級直接換成libSystem.C.dylib
然後再替換替身就行了 - 減少可執行文件體積:相比靜態鏈接,可執行文件的體積要小很多
dyld
dyld
- the dynamic link editor(這縮寫對應的很奇怪,我感覺是DYnamic Linker Daemon呢- -?)apple的動態鏈接器,系統kernel做好啟動程序的初始準備後,交給dyld負責,援引並翻譯《mikeask這篇blog》對dyld作用順序的概括:
- 從kernel留下的原始調用棧引導和啟動自己
- 將程序依賴的動態鏈接庫
遞歸
加載進內存,當然這裏有緩存機制
- non-lazy符號立即link到可執行文件,lazy的存表裏
- Runs static initializers for the executable
- 找到可執行文件的main函數,準備參數並調用
- 程序執行中負責綁定lazy符號、提供runtime dynamic loading services、提供調試器接口
- 程序main函數return後執行static terminator
- 某些場景下main函數結束後調libSystem的_exit函數
得益於dyld是開源的,github地址,我們可以從源碼一探究竟。
一切源於dyldStartup.s
這個文件,其中用匯編實現了名為__dyld_start
的方法,匯編太生澀,它主要幹了兩件事:
- 調用
dyldbootstrap::start()
方法(省去參數) - 上個方法返回了main函數地址,填入參數並調用main函數
這個步驟隨手就能驗證出來,設置一個符號斷點
斷在_objc_init
:
這個函數是runtime
的初始化函數,後面會提到。程序運行在很早的時候斷住,這時候看調用棧:
看到了棧底的dyldbootstrap::start()
方法,繼而調用了dyld::_main()
方法,其中完成了剛才說的遞歸加載動態庫過程,由於libSystem
默認引入,棧中出現了libSystem_initializer
的初始化方法。
ImageLoader
當然這個image不是圖片的意思,它大概表示一個二進制文件(可執行文件或so文件),裏面是被編譯過的符號、代碼等,所以ImageLoader
作用是將這些文件加載進內存,且每一個文件對應一個ImageLoader實例來負責加載。
兩步走:
- 在程序運行時它先將動態鏈接的image遞歸加載 (也就是上面測試棧中一串的遞歸調用的時刻)
- 再從可執行文件image遞歸加載所有符號
當然所有這些都發生在我們真正的main函數執行前。
runtime與+load
剛才講到libSystem
是若幹個系統lib的集合,所以它只是一個容器lib而已,而且它也是開源的,裏面實質上就一個文件,init.c,細節不說了,由libSystem_initializer
逐步調用到了_objc_init
,這裏就是objc和runtime的初始化入口。
除了runtime環境的初始化外,_objc_init
中綁定了新image被加載後的callback:
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
函數:
清楚的看到整個調用棧和順序:
- dyld開始將程序二進制文件初始化
- 交由ImageLoader讀取image,其中包含了我們的類、方法等各種符號
- 由於runtime向dyld綁定了回調,當image加載到內存後,dyld會通知runtime進行處理
- runtime接手後調用map_images做解析和處理,接下來load_images中調用call_load_methods方法,遍歷所有加載進來的Class,按繼承層級依次調用Class的load方法和其Category的load方法
至此,可執行文件中和動態庫所有的符號(Class,Protocol,Selector,IMP,…)都已經按格式成功加載到內存中,被runtime所管理,再這之後,runtime的那些方法(動態添加Class、方法混合等等才能生效)
關於load方法的幾個QA
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函數執行之前,系統做了茫茫多的加載和初始化工作,但都被很好的隱藏了,我們無需關心。
孤獨的main函數
當這一切都結束時,dyld會清理現場,將調用棧回歸,只剩下:
孤獨的main函數,看上去是程序的開始,確是一段精彩的終結
iOS程序main函數之前發生了什麽