我是前言
一個 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
-L 參數打印出所有 link 的 framework(去掉了版本信息如下)
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 ),系統使用動態鏈接有幾點好處:
- 代碼共用:很多程序都動態鏈接了這些 lib,但它們在內存和磁盤中中只有一份
- 易于維護:由于被依賴的 lib 是程序執行時才 link 的,所以這些 lib 很容易做更新,比如
libSystem.dylib
是libSystem.B.dylib
的替身,哪天想升級直接換成libSystem.C.dylib
然后再替換替身就行了 - 減少可執行文件體積:相比靜態鏈接,動態鏈接在編譯時不需要打進去,所以可執行文件的體積要小很多
dyld
dyld(the dynamic link editor),Apple 的動態鏈接器,系統 kernel 做好啟動程序的初始準備后,交給 dyld 負責,援引并翻譯《 Mike Ash 這篇 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, &map_images);
dyld_register_image_state_change_handler(
dyld_image_state_dependents_initialized, 0, &load_images);
可見 dyld 擔當了 runtime
和 ImageLoader
中間的協調者,當新 image 加載進來后交由 runtime 大廚去解析這個二進制文件的符號表和代碼。繼續上面的斷點法,斷住神秘的 +load
函數:
image
清楚的看到整個調用棧和順序:
- 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、swizzle 等等才能生效)
關于 +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 會清理現場,將調用?;貧w,只剩下:
孤獨的 main 函數,看上去是程序的開始,確是一段精彩的終結
References
https://www.mikeash.com/pyblog/friday-qa-2012-11-09-dyld-dynamic-linking-on-os-x.html
http://newosxbook.com/articles/DYLD.html
http://docstore.mik.ua/orelly/unix3/mac/ch05_02.htm
https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dyld.1.html
聲明
此文引用自iOS程序 main 函數之前發生了什么,自從業以來,從孫源老師那學到了很多,在此表示由衷的感謝!
其他拓展
dyld:dyld詳解