衆所周知,一個app的入口就是main.m 裏面的main函數,接下來我們來剖根究底的探討下調用main函數之前,程序都做了哪些事情?
動態鏈接庫
iOS 中用到的所有系統 framework 都是動態鏈接的,類比成插頭和插排,靜態鏈接的代碼在編譯後的靜態鏈接過程就將插頭和插排一個個插好,運行時直接執行二進制文件;而動態鏈接需要在程序啓動時去完成“插插銷”的過程,所以在我們寫的代碼執行前,動態連接器需要完成準備工作。
使用otool 可以查看隱藏的動態鏈接庫
系統使用動態鏈接有幾點好處:
- 代碼共用:很多程序都動態鏈接了這些 lib,但它們在內存和磁盤中中只有一份
- 易於維護:由於被依賴的 lib 是程序執行時才 link 的,所以這些 lib 很容易做更新,比如
libSystem.dylib
是libSystem.B.dylib
的替身,哪天想升級直接換成libSystem.C.dylib
然後再替換替身就行了 - 減少可執行文件體積:相比靜態鏈接,動態鏈接在編譯時不需要打進去,所以可執行文件的體積要小很多
dyld 動態鏈接器
系統核心做好啓動程序的準備工作後,交給dylb負責,對dylb的作用順序概括如下:
- 從 kernel 留下的原始調用棧引導和啓動自己
- 將程序依賴的動態鏈接庫遞歸加載進內存,當然這裏有緩存機制
- 使用imageLoader將二進制文件(可執行文件或so文件),裏面是編譯過的符號代碼等加載進內存,且每一個文件對應一個imageLoader實例來負責加載。dyld 會通知 runtime 進行處理,runtime 接手後調用 map_images 做解析和處理,接下來 load_images 中調用 call_load_methods 方法,遍歷所有加載進來的 Class,按繼承層級依次調用 Class 的 +load 方法和其 Category 的 +load 方法
- non-lazy 符號立即 link 到可執行文件,lazy 的存表裏
- Runs static initializers for the executable
- 找到可執行文件的 main 函數,準備參數並調用
- 程序執行中負責綁定 lazy 符號、提供 runtime dynamic loading services、提供調試器接口
- 程序main函數 return 後執行 static terminator
- 某些場景下 main 函數結束後調 libSystem 的 _exit 函數
總結
整個事件由 dyld 主導,完成運行環境的初始化後,配合 ImageLoader 將二進制文件按格式加載到內存,
動態鏈接依賴庫,並由 runtime 負責加載成 objc 定義的結構,所有初始化工作結束後,dyld 調用真正的 main 函數。
值得說明的是,這個過程遠比寫出來的要複雜,這裏只提到了 runtime 這個分支,還有像 GCD
、XPC
等重頭的系統庫初始化分支沒有提及(當然,有緩存機制在,它們也不會玩命初始化),總結起來就是 main 函數執行之前,系統做了茫茫多的加載和初始化工作,但都被很好的隱藏了,我們無需關心。