iOS dyld流程分析

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

引子

  • 創(chuàng)建一個(gè)project,在ViewController中重寫了load方法鹿霸,在main中加了一個(gè)C++方法怠噪,即kcFUnc,請(qǐng)問它們的打印先后順序是什么杜跷?

    問題引入
  • 運(yùn)行程序,查看 load矫夷、kcFunc葛闷、main的打印順序,下面是打印結(jié)果双藕,通過結(jié)果可以看出其順序是 load --> C++方法 --> main

為什么是這么一個(gè)順序淑趾?按照常規(guī)的思維理解,main不是入口函數(shù)嗎忧陪?為什么不是main最先執(zhí)行?

下面根據(jù)這個(gè)問題嘶摊,我們來探索在走到main之前延蟹,到底還做了什么。

編譯過程&庫

在分析app啟動(dòng)之前叶堆,我們需要先了解iOS app的編譯過程以及動(dòng)態(tài)庫靜態(tài)庫阱飘。

編譯過程

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

  • 源文件:載入.h虱颗、.m沥匈、.cpp等文件

  • 預(yù)處理:替換宏,刪除注釋忘渔,展開頭文件高帖,產(chǎn)生.i文件

  • 編譯:將.i文件轉(zhuǎn)換為匯編語言,產(chǎn)生.s文件

  • 匯編:將匯編文件轉(zhuǎn)換為機(jī)器碼文件畦粮,產(chǎn)生.o文件

  • 鏈接:對(duì).o文件中引用其他庫的地方進(jìn)行引用散址,生成最后的可執(zhí)行文件

    編譯過程

靜態(tài)庫 和 動(dòng)態(tài)庫

  • 靜態(tài)庫:在鏈接階段,會(huì)將可匯編生成的目標(biāo)程序與引用的庫一起鏈接打包到可執(zhí)行文件當(dāng)中宣赔。此時(shí)的靜態(tài)庫就不會(huì)在改變了爪飘,因?yàn)樗蔷幾g時(shí)被直接拷貝一份,復(fù)制到目標(biāo)程序里的

    • 好處:編譯完成后拉背,庫文件實(shí)際上就沒有作用了师崎,目標(biāo)程序沒有外部依賴,直接就可以運(yùn)行

    • 缺點(diǎn):由于靜態(tài)庫會(huì)有兩份椅棺,所以會(huì)導(dǎo)致目標(biāo)程序的體積增大犁罩,對(duì)內(nèi)存齐蔽、性能、速度消耗很大

  • 動(dòng)態(tài)庫:程序編譯時(shí)并不會(huì)鏈接到目標(biāo)程序中床估,目標(biāo)程序只會(huì)存儲(chǔ)指向動(dòng)態(tài)庫的引用含滴,在程序運(yùn)行時(shí)才被載入

    • 優(yōu)勢(shì)

      • 減少打包之后app的大小:因?yàn)椴恍枰截愔聊繕?biāo)程序中,所以不會(huì)影響目標(biāo)程序的體積丐巫,與靜態(tài)庫相比谈况,減少了app的體積大小

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

      • 通過更新動(dòng)態(tài)庫递胧,達(dá)到更新程序的目的:由于運(yùn)行時(shí)才載入的特性碑韵,可以隨時(shí)對(duì)庫進(jìn)行替換,而不需要重新編譯代碼

    • 缺點(diǎn):動(dòng)態(tài)載入會(huì)帶來一部分性能損失缎脾,使用動(dòng)態(tài)庫也會(huì)使得程序依賴于外部環(huán)境祝闻,如果環(huán)境缺少了動(dòng)態(tài)庫,或者庫的版本不正確遗菠,就會(huì)導(dǎo)致程序無法運(yùn)行

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

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

dyld加載流程分析

根據(jù)dyld源碼联喘,以及libobjclibSystem辙纬、libdispatch源碼協(xié)同分析

什么是dyld豁遭?

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

所以 App的啟動(dòng)流程圖如下

app啟動(dòng)的起始點(diǎn)

  • 在前文的demo中纵柿,在load方法處加一個(gè)斷點(diǎn)蜈抓,通過bt堆棧信息查看app啟動(dòng)是從哪里開始的

    app啟動(dòng)起點(diǎn)

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

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

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

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

源碼中搜索dyldbootstrap找到命名作用空間腊嗡,再在這個(gè)文件中查找start方法,其核心是返回值的調(diào)用了dyldmain函數(shù)拾酝,其中macho_headerMach-O的頭部燕少,而dyld加載的文件就是Mach-O類型的,即Mach-O類型是可執(zhí)行文件類型蒿囤,由四部分組成:Mach-O頭部客们、Load Command、section、Other Data,可以通過MachOView查看可執(zhí)行文件信息

進(jìn)入dyld::_main的源碼實(shí)現(xiàn),特別長(zhǎng)待牵,大約600多行,如果對(duì)dyld加載流程不太了解的童鞋凤优,可以根據(jù)_main函數(shù)的返回值進(jìn)行反推,這里就多作說明。在_main函數(shù)中主要做了一下幾件事情:

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

    第一步
  • 【第二步:共享緩存】:檢查是否開啟了共享緩存,以及共享緩存是否映射到共享區(qū)域沸手,例如UIKitCoreFoundation

第二步
  • 【第三步:主程序的初始化】:調(diào)用instantiateFromLoadedImage函數(shù)實(shí)例化了一個(gè)ImageLoader對(duì)象
第三步
  • 【第四步:插入動(dòng)態(tài)庫】:遍歷DYLD_INSERT_LIBRARIES環(huán)境變量注簿,調(diào)用loadInsertedDylib加載
第四步
  • 【第五步:link 主程序
第五步
  • 【第六步:link 動(dòng)態(tài)庫
第六步
  • 【第七步:弱符號(hào)綁定
第七步
  • 【第八步:執(zhí)行初始化方法
image

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

第九步

第三步:主程序初始化

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

進(jìn)入instantiateFromLoadedImage源碼玩徊,其中創(chuàng)建一個(gè)ImageLoader實(shí)例對(duì)象,通過instantiateMainExecutable方法創(chuàng)建

進(jìn)入instantiateMainExecutable源碼谨究,其作用是為主可執(zhí)行文件創(chuàng)建映像恩袱,返回一個(gè)ImageLoader類型的image對(duì)象,即主程序胶哲。其中sniffLoadCommands函數(shù)時(shí)獲取Mach-O類型文件Load Command的相關(guān)信息畔塔,并對(duì)其進(jìn)行各種校驗(yàn)

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

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

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

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

在這里澈吨,需要分成兩部分探索,一部分是notifySingle函數(shù)寄摆,一部分是doInitialization函數(shù)谅辣,首先探索notifySingle函數(shù)

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

全局搜索sNotifyObjCInit,發(fā)現(xiàn)沒有找到實(shí)現(xiàn)婶恼,有賦值操作

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

注意:_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會(huì)調(diào)用所有的+load方法眷篇。所以綜上所述萎河,notifySingle是一個(gè)回調(diào)函數(shù)

load函數(shù)加載

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

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

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

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

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

【總結(jié)】load的源碼鏈為:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一個(gè)回調(diào)處理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
那么問題又來了沾鳄,_objc_init是什么時(shí)候調(diào)用的呢?請(qǐng)接著往下看

走到objc_objc_init函數(shù)确憨,發(fā)現(xiàn)走不通了译荞,我們回退到recursiveInitialization遞歸函數(shù)的源碼實(shí)現(xiàn),發(fā)現(xiàn)我們忽略了一個(gè)函數(shù)doInitialization

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

這里也需要分成兩部分休弃,一部分是doImageInit函數(shù)吞歼,一部分是doModInitFunctions函數(shù)

  • 進(jìn)入doImageInit源碼實(shí)現(xiàn),其核心主要是for循環(huán)加載方法的調(diào)用塔猾,這里需要注意的一點(diǎn)是篙骡,libSystem的初始化必須先運(yùn)行

進(jìn)入doModInitFunctions源碼實(shí)現(xiàn),這個(gè)方法中加載了所有C++文件

可以通過測(cè)試程序的堆棧信息來驗(yàn)證,在C++方法處加一個(gè)斷點(diǎn)

    • image

走到這里丈甸,還是沒有找到_objc_init的調(diào)用糯俗?怎么辦呢?放棄嗎睦擂?當(dāng)然不行得湘,我們還可以通過_objc_init加一個(gè)符號(hào)斷點(diǎn)來查看調(diào)用_objc_init前的堆棧信息,

  • _objc_init加一個(gè)符號(hào)斷點(diǎn)顿仇,運(yùn)行程序淘正,查看_objc_init斷住后的堆棧信息

    image

libsystem中查找libSystem_initializer,查看其中的實(shí)現(xiàn)

根據(jù)前面的堆棧信息臼闻,我們發(fā)現(xiàn)走的是libSystem_initializer中會(huì)調(diào)用libdispatch_init函數(shù)鸿吆,而這個(gè)函數(shù)的源碼是在libdispatch開源庫中的,在libdispatch中搜索libdispatch_init

    • image

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

    • image

結(jié)合上面的分析伞剑,從初始化_objc_init注冊(cè)的_dyld_objc_notify_register的參數(shù)2,即load_images市埋,到sNotifySingle --> sNotifyObjCInie=參數(shù)2sNotifyObjcInit()調(diào)用黎泣,形成了一個(gè)閉環(huán)

所以可以簡(jiǎ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(li#### 第九步:尋找主入口函數(shù)

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

    [圖片上傳中...(image-5e2d0b-1613204313417-0)]
    bSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

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

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

    image

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

  • image

點(diǎn)擊stepover,繼續(xù)往下,跑完了整個(gè)流程项郊,會(huì)回到_dyld_start,然后調(diào)用main()函數(shù),通過匯編完成main的參數(shù)賦值等操作

dyld匯編源碼實(shí)現(xiàn)

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

注意:main是寫定的函數(shù)馅扣,寫入內(nèi)存,讀取到dyld着降,如果修改了main函數(shù)的名稱差油,會(huì)報(bào)錯(cuò)

    • image

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

    • dyld加載流程
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末交掏,一起剝皮案震驚了整個(gè)濱河市妆偏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盅弛,老刑警劉巖钱骂,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異挪鹏,居然都是意外死亡见秽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門狰住,熙熙樓的掌柜王于貴愁眉苦臉地迎上來张吉,“玉大人齿梁,你說我怎么就攤上這事催植。” “怎么了勺择?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵创南,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我省核,道長(zhǎng)稿辙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任气忠,我火速辦了婚禮邻储,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘旧噪。我一直安慰自己吨娜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布淘钟。 她就那樣靜靜地躺著宦赠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上勾扭,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天毡琉,我揣著相機(jī)與錄音,去河邊找鬼妙色。 笑死桅滋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的燎斩。 我是一名探鬼主播虱歪,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼栅表!你這毒婦竟也來了笋鄙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤怪瓶,失蹤者是張志新(化名)和其女友劉穎萧落,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洗贰,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡找岖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了敛滋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片许布。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖绎晃,靈堂內(nèi)的尸體忽然破棺而出蜜唾,到底是詐尸還是另有隱情,我是刑警寧澤庶艾,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布袁余,位于F島的核電站,受9級(jí)特大地震影響咱揍,放射性物質(zhì)發(fā)生泄漏颖榜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一煤裙、第九天 我趴在偏房一處隱蔽的房頂上張望掩完。 院中可真熱鬧,春花似錦硼砰、人聲如沸且蓬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缅疟。三九已至分别,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間存淫,已是汗流浹背耘斩。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留桅咆,地道東北人括授。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像岩饼,于是被迫代替她去往敵國(guó)和親荚虚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

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