標籤:設定 pat 分享圖片 pop 依賴 mod service 迴歸 get
我是前言
一個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函數之前發生了什麼