iOS-底層原理 15:dyld加載流程

iOS 底層原理 文章匯總

本文的目的主要是分析dyld的加載流程囚巴,了解在main函數(shù)之前,底層還做了什么

引子

  • 創(chuàng)建一個project氢卡,在ViewController中重寫了load方法,在main中加了一個C++方法,即kcFUnc,請問它們的打印先后順序是什么寓涨?

    問題引入

  • 運行程序,查看 load氯檐、kcFunc戒良、main的打印順序,下面是打印結(jié)果冠摄,通過結(jié)果可以看出其順序是 load --> C++方法 --> main

    打印結(jié)果

為什么是這么一個順序糯崎?按照常規(guī)的思維理解,main不是入口函數(shù)嗎河泳?為什么不是main最先執(zhí)行拇颅?

下面根據(jù)這個問題,我們來探索在走到main之前乔询,到底還做了什么。

編譯過程及庫

在分析app啟動之前韵洋,我們需要先了解iOSapp代碼的編譯過程以及動態(tài)庫靜態(tài)庫竿刁。

編譯過程

其中編譯過程如下圖所示,主要分為以下幾步:

  • 源文件:載入.h搪缨、.m食拜、.cpp等文件
  • 預(yù)處理:替換宏,刪除注釋副编,展開頭文件负甸,產(chǎn)生.i文件
  • 編譯:將.i文件轉(zhuǎn)換為匯編語言,產(chǎn)生.s文件
  • 匯編:將匯編文件轉(zhuǎn)換為機(jī)器碼文件痹届,產(chǎn)生.o文件
  • 鏈接:對.o文件中引用其他庫的地方進(jìn)行引用呻待,生成最后的可執(zhí)行文件
    編譯過程

靜態(tài)庫 和 動態(tài)庫

  • 靜態(tài)庫:在鏈接階段,會將可匯編生成的目標(biāo)程序與引用的庫一起鏈接打包到可執(zhí)行文件當(dāng)中队腐。此時的靜態(tài)庫就不會在改變了蚕捉,因為它是編譯時被直接拷貝一份,復(fù)制到目標(biāo)程序里的
    • 好處:編譯完成后柴淘,庫文件實際上就沒有作用了迫淹,目標(biāo)程序沒有外部依賴秘通,直接就可以運行

    • 缺點:由于靜態(tài)庫會有兩份,所以會導(dǎo)致目標(biāo)程序的體積增大敛熬,對內(nèi)存肺稀、性能、速度消耗很大

  • 動態(tài)庫:程序編譯時并不會鏈接到目標(biāo)程序中应民,目標(biāo)程序只會存儲指向動態(tài)庫的引用话原,在程序運行時才被載入
    • 優(yōu)勢
      • 減少打包之后app的大小:因為不需要拷貝至目標(biāo)程序中,所以不會影響目標(biāo)程序的體積瑞妇,與靜態(tài)庫相比稿静,減少了app的體積大小

      • 共享內(nèi)存,節(jié)約資源:同一份庫可以被多個程序使用

      • 通過更新動態(tài)庫辕狰,達(dá)到更新程序的目的:由于運行時才載入的特性改备,可以隨時對庫進(jìn)行替換,而不需要重新編譯代碼

    • 缺點:動態(tài)載入會帶來一部分性能損失蔓倍,使用動態(tài)庫也會使得程序依賴于外部環(huán)境悬钳,如果環(huán)境缺少了動態(tài)庫,或者庫的版本不正確偶翅,就會導(dǎo)致程序無法運行

靜態(tài)庫和動態(tài)庫的圖示如圖所示


靜態(tài)庫和動態(tài)庫圖示

dyld加載流程分析

根據(jù)dyld源碼默勾,以及libobjclibSystem聚谁、libdispatch源碼協(xié)同分析

什么是dyld母剥?

dyld(the dynamic link editor)是蘋果的動態(tài)鏈接器,是蘋果操作系統(tǒng)的重要組成部分形导,在app被編譯打包成可執(zhí)行文件格式的Mach-O文件后环疼,交由dyld負(fù)責(zé)連接,加載程序

所以 App的啟動流程圖如下


App啟動流程

app啟動的起始點

  • 在前文的demo中朵耕,在load方法處加一個斷點炫隶,通過bt堆棧信息查看app啟動是從哪里開始的

    app啟動起點

    【app啟動起點】:通過程序運行發(fā)現(xiàn),是從dyld中的_dyld_start開始的阎曹,所以需要去OpenSource下載一份dyld的源碼來進(jìn)行分析

  • 也可以通過xcode左側(cè)的堆棧信息來找到入口


    xcode堆棧信息

dyld::_main函數(shù)源碼分析

  • dyld-750.6源碼中查找_dyld_start,查找arm64架構(gòu)發(fā)現(xiàn)伪阶,是由匯編實現(xiàn),通過匯編注釋發(fā)現(xiàn)會調(diào)用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)方法处嫌,是一個C++方法(以arm64架構(gòu)為例)

    dyldbootstrap::start源碼

  • 源碼中搜索dyldbootstrap找到命名作用空間栅贴,再在這個文件中查找start方法,其核心是返回值的調(diào)用了dyldmain函數(shù)熏迹,其中macho_headerMach-O的頭部筹误,而dyld加載的文件就是Mach-O類型的,即Mach-O類型是可執(zhí)行文件類型癣缅,由四部分組成:Mach-O頭部厨剪、Load Command哄酝、section、Other Data祷膳,可以通過MachOView查看可執(zhí)行文件信息

    _main源碼實現(xiàn)

  • 進(jìn)入dyld::_main的源碼實現(xiàn)陶衅,特別長,大約600多行直晨,如果對dyld加載流程不太了解的童鞋搀军,可以根據(jù)_main函數(shù)的返回值進(jìn)行反推,這里就多作說明勇皇。在_main函數(shù)中主要做了一下幾件事情:

    • 【第一步:環(huán)境變量配置】:根據(jù)環(huán)境變量設(shè)置相應(yīng)的值以及獲取當(dāng)前運行架構(gòu)

      第一步

    • 【第二步:共享緩存】:檢查是否開啟了共享緩存罩句,以及共享緩存是否映射到共享區(qū)域,例如UIKit敛摘、CoreFoundation

      第二步

    • 【第三步:主程序的初始化】:調(diào)用instantiateFromLoadedImage函數(shù)實例化了一個ImageLoader對象

      第三步

    • 【第四步:插入動態(tài)庫】:遍歷DYLD_INSERT_LIBRARIES環(huán)境變量门烂,調(diào)用loadInsertedDylib加載

      第四步

    • 【第五步:link 主程序

      第五步

    • 【第六步:link 動態(tài)庫

      第六步

    • 【第七步:弱符號綁定

      第七步

    • 【第八步:執(zhí)行初始化方法

      第八步

    • 【第九步:尋找主程序入口main函數(shù)】:從Load Command讀取LC_MAIN入口,如果沒有兄淫,就讀取LC_UNIXTHREAD屯远,這樣就來到了日常開發(fā)中熟悉的main函數(shù)了

      第九步

下面主要分析下【第三步】和【第八步】

第三步:主程序初始化

  • sMainExecutable表示主程序變量,查看其賦值捕虽,是通過instantiateFromLoadedImage方法初始化

    instantiateFromLoadedImage初始化主程序

  • 進(jìn)入instantiateFromLoadedImage源碼慨丐,其中創(chuàng)建一個ImageLoader實例對象,通過instantiateMainExecutable方法創(chuàng)建

    instantiateFromLoadedImage源碼實現(xiàn)

  • 進(jìn)入instantiateMainExecutable源碼泄私,其作用是為主可執(zhí)行文件創(chuàng)建映像房揭,返回一個ImageLoader類型的image對象,即主程序晌端。其中sniffLoadCommands函數(shù)時獲取Mach-O類型文件Load Command的相關(guān)信息捅暴,并對其進(jìn)行各種校驗

    instantiateMainExecutable源碼實現(xiàn)

第八步:執(zhí)行初始化方法

  • 進(jìn)入initializeMainExecutable源碼,主要是循環(huán)遍歷,都會執(zhí)行runInitializers方法

    initializeMainExecutable源碼實現(xiàn)

  • 全局搜索runInitializers(cons,找到如下源碼斩松,其核心代碼是processInitializers函數(shù)的調(diào)用

    runInitializers源碼實現(xiàn)

  • 進(jìn)入processInitializers函數(shù)的源碼實現(xiàn),其中對鏡像列表調(diào)用recursiveInitialization函數(shù)進(jìn)行遞歸實例化

    processInitializers源碼實現(xiàn)

  • 全局搜索recursiveInitialization(cons函數(shù),其源碼實現(xiàn)如下

    recursiveInitialization源碼實現(xiàn)

在這里觉既,需要分成兩部分探索惧盹,一部分是notifySingle函數(shù),一部分是doInitialization函數(shù)瞪讼,首先探索notifySingle函數(shù)

notifySingle 函數(shù)
  • 全局搜索notifySingle(函數(shù),其重點是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());這句

    notifySingle源碼實現(xiàn)

  • 全局搜索sNotifyObjCInit钧椰,發(fā)現(xiàn)沒有找到實現(xiàn),有賦值操作

    registerObjCNotifiers源碼實現(xiàn)

  • 搜索registerObjCNotifiers在哪里調(diào)用了符欠,發(fā)現(xiàn)在_dyld_objc_notify_register進(jìn)行了調(diào)用

    _dyld_objc_notify_register源碼實現(xiàn)

    注意:_dyld_objc_notify_register的函數(shù)需要在libobjc源碼中搜索

  • objc4-781源碼中搜索_dyld_objc_notify_register嫡霞,發(fā)現(xiàn)在_objc_init源碼中調(diào)用了該方法,并傳入了參數(shù)希柿,所以sNotifyObjCInit賦值的就是objc中的load_images诊沪,而load_images會調(diào)用所有的+load方法养筒。所以綜上所述,notifySingle是一個回調(diào)函數(shù)

    _objc_init源碼實現(xiàn)

load函數(shù)加載

下面我們進(jìn)入load_images的源碼看看其實現(xiàn)端姚,以此來證明load_images中調(diào)用了所有的load函數(shù)

  • 通過objc源碼中_objc_init源碼實現(xiàn)晕粪,進(jìn)入load_images的源碼實現(xiàn)

    load_images源碼實現(xiàn)

  • 進(jìn)入call_load_methods源碼實現(xiàn),可以發(fā)現(xiàn)其核心是通過do-while循環(huán)調(diào)用+load方法

    call_load_methods源碼實現(xiàn)

  • 進(jìn)入call_class_loads源碼實現(xiàn)渐裸,了解到這里調(diào)用的load方法證實我們前文提及的類的load方法

    call_class_loads源碼實現(xiàn)

所以巫湘,load_images調(diào)用了所有的load函數(shù),以上的源碼分析過程正好對應(yīng)堆棧的打印信息

堆棧信息

【總結(jié)】load的源碼鏈為:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一個回調(diào)處理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)

那么問題又來了昏鹃,_objc_init是什么時候調(diào)用的呢尚氛?請接著往下看

doInitialization 函數(shù)
  • 走到objc_objc_init函數(shù)成艘,發(fā)現(xiàn)走不通了戏阅,我們回退到recursiveInitialization遞歸函數(shù)的源碼實現(xiàn),發(fā)現(xiàn)我們忽略了一個函數(shù)doInitialization

    recursiveInitialization源碼實現(xiàn)

  • 進(jìn)入doInitialization函數(shù)的源碼實現(xiàn)

    doInitialization源碼實現(xiàn)

    這里也需要分成兩部分闸婴,一部分是doImageInit函數(shù)您宪,一部分是doModInitFunctions函數(shù)

    • 進(jìn)入doImageInit源碼實現(xiàn)奈懒,其核心主要是for循環(huán)加載方法的調(diào)用,這里需要注意的一點是宪巨,libSystem的初始化必須先運行
      doImageInit源碼實現(xiàn)
    • 進(jìn)入doModInitFunctions源碼實現(xiàn)磷杏,這個方法中加載了所有Cxx文件
      doModInitFunctions源碼實現(xiàn)

      可以通過測試程序的堆棧信息來驗證,在C++方法處加一個斷點
      C++斷點堆棧信息

走到這里,還是沒有找到_objc_init的調(diào)用捏卓?怎么辦呢极祸?放棄嗎?當(dāng)然不行怠晴,我們還可以通過_objc_init加一個符號斷點來查看調(diào)用_objc_init前的堆棧信息遥金,

  • _objc_init加一個符號斷點,運行程序蒜田,查看_objc_init斷住后的堆棧信息

    _objc_init符號斷點堆棧信息

  • libsystem中查找libSystem_initializer稿械,查看其中的實現(xiàn)

    libSystem_initializer源碼實現(xiàn)

  • 根據(jù)前面的堆棧信息,我們發(fā)現(xiàn)走的是libSystem_initializer中會調(diào)用libdispatch_init函數(shù)冲粤,而這個函數(shù)的源碼是在libdispatch開源庫中的美莫,在libdispatch中搜索libdispatch_init

    libdispatch_init源碼實現(xiàn)

  • 進(jìn)入_os_object_init源碼實現(xiàn),其源碼實現(xiàn)調(diào)用了_objc_init函數(shù)

    _os_object_init源碼實現(xiàn)

    結(jié)合上面的分析梯捕,從初始化_objc_init注冊的_dyld_objc_notify_register的參數(shù)2厢呵,即load_images,到sNotifySingle --> sNotifyObjCInie=參數(shù)2sNotifyObjcInit()調(diào)用傀顾,形成了一個閉環(huán)

所以可以簡單的理解為sNotifySingle這里是添加通知即addObserver襟铭,_objc_init中調(diào)用_dyld_objc_notify_register相當(dāng)于發(fā)送通知,即push,而sNotifyObjcInit相當(dāng)于通知的處理函數(shù)寒砖,即selector

【總結(jié)】:_objc_init的源碼鏈:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

第九步:尋找主入口函數(shù)

  • 匯編調(diào)試赐劣,可以看到顯示來到+[ViewController load]方法

    匯編調(diào)試-load

  • 繼續(xù)執(zhí)行,來到kcFunc的C++函數(shù)

    匯編調(diào)試-kcFunc

  • 點擊stepover,繼續(xù)往下入撒,跑完了整個流程隆豹,會回到_dyld_start,然后調(diào)用main()函數(shù),通過匯編完成main的參數(shù)賦值等操作

    匯編調(diào)試回到_dyld_start

    dyld匯編源碼實現(xiàn)
    dyld中main部分的匯編源碼實現(xiàn)

注意:main是寫定的函數(shù),寫入內(nèi)存茅逮,讀取到dyld璃赡,如果修改了main函數(shù)的名稱,會報錯

報錯信息

所以献雅,綜上所述碉考,最終dyld加載流程,如下圖所示挺身,圖中也詮釋了前文中的問題:為什么是load-->Cxx-->main的調(diào)用順序

dyld加載流程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侯谁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子章钾,更是在濱河造成了極大的恐慌墙贱,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贱傀,死亡現(xiàn)場離奇詭異惨撇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)府寒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門魁衙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人株搔,你說我怎么就攤上這事剖淀。” “怎么了纤房?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵纵隔,是天一觀的道長。 經(jīng)常有香客問我炮姨,道長捌刮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任剑令,我火速辦了婚禮糊啡,結(jié)果婚禮上拄查,老公的妹妹穿的比我還像新娘吁津。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布碍脏。 她就那樣靜靜地躺著梭依,像睡著了一般。 火紅的嫁衣襯著肌膚如雪典尾。 梳的紋絲不亂的頭發(fā)上役拴,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音钾埂,去河邊找鬼河闰。 笑死,一個胖子當(dāng)著我的面吹牛褥紫,可吹牛的內(nèi)容都是我干的姜性。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼髓考,長吁一口氣:“原來是場噩夢啊……” “哼部念!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起氨菇,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤儡炼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后查蓉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乌询,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年奶是,在試婚紗的時候發(fā)現(xiàn)自己被綠了楣责。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡聂沙,死狀恐怖秆麸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情及汉,我是刑警寧澤沮趣,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站坷随,受9級特大地震影響房铭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜温眉,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一缸匪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧类溢,春花似錦凌蔬、人聲如沸露懒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懈词。三九已至,卻和暖如春辩诞,著一層夾襖步出監(jiān)牢的瞬間坎弯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工译暂, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留抠忘,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓外永,卻偏偏與公主長得像褐桌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子象迎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容