iOS程序main函數以前發生了什麼

我是前言

一個iOS app的main()函數位於main.m中,這是咱們熟知的程序入口。但對objc瞭解更多以後發現,程序在進入咱們的main函數前已經執行了不少代碼,好比熟知的+ load方法等。本文將跟隨程序執行順序,刨根問底,從dyld到runtime,看看main函數以前都發生了什麼。 html


從dyld開始

動態連接庫

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

  • 代碼共用:不少程序都動態連接了這些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做用順序的歸納: xcode

  1. 從kernel留下的原始調用棧引導和啓動本身
  2. 將程序依賴的動態連接庫遞歸加載進內存,固然這裏有緩存機制
  3. non-lazy符號當即link到可執行文件,lazy的存表裏
  4. Runs static initializers for the executable
  5. 找到可執行文件的main函數,準備參數並調用
  6. 程序執行中負責綁定lazy符號、提供runtime dynamic loading services、提供調試器接口
  7. 程序main函數return後執行static terminator
  8. 某些場景下main函數結束後調libSystem的_exit函數

得益於dyld是開源的,github地址,咱們能夠從源碼一探究竟。 緩存

一切源於dyldStartup.s這個文件,其中用匯編實現了名爲__dyld_start的方法,彙編太生澀,它主要乾了兩件事: app

  1. 調用dyldbootstrap::start()方法(省去參數)
  2. 上個方法返回了main函數地址,填入參數並調用main函數

這個步驟隨手就能驗證出來,設置一個符號斷點斷在_objc_init:

這個函數是runtime的初始化函數,後面會提到。程序運行在很早的時候斷住,這時候看調用棧:

看到了棧底的dyldbootstrap::start()方法,繼而調用了dyld::_main()方法,其中完成了剛纔說的遞歸加載動態庫過程,因爲libSystem默認引入,棧中出現了libSystem_initializer的初始化方法。 函數

ImageLoader

固然這個image不是圖片的意思,它大概表示一個二進制文件(可執行文件或so文件),裏面是被編譯過的符號、代碼等,因此ImageLoader做用是將這些文件加載進內存,且每個文件對應一個ImageLoader實例來負責加載
兩步走: 測試

  1. 在程序運行時它先將動態連接的image遞歸加載 (也就是上面測試棧中一串的遞歸調用的時刻)
  2. 再從可執行文件image遞歸加載全部符號

固然全部這些都發生在咱們真正的main函數執行前。


runtime與+load

剛纔講到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函數:

清楚的看到整個調用棧和順序:

  1. dyld開始將程序二進制文件初始化
  2. 交由ImageLoader讀取image,其中包含了咱們的類、方法等各類符號
  3. 因爲runtime向dyld綁定了回調,當image加載到內存後,dyld會通知runtime進行處理
  4. 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函數,看上去是程序的開始,確是一段精彩的終結

相關文章
相關標籤/搜索