iOS dyld流程分析

本文的目的主要是分析dyld的加載流程慢逾,了解在main函數之前边灭,底層還做了什么

引子

  • 創(chuàng)建一個project煞额,在ViewController中重寫了load方法波闹,在main中加了一個C++方法一喘,即kcFUnc逸贾,請問它們的打印先后順序是什么?

    image
  • 運行程序津滞,查看 load铝侵、kcFunc、main的打印順序触徐,下面是打印結果咪鲜,通過結果可以看出其順序是 load --> C++方法 --> main

    • image

為什么是這么一個順序?按照常規(guī)的思維理解撞鹉,main不是入口函數嗎疟丙?為什么不是main最先執(zhí)行?

下面根據這個問題鸟雏,我們來探索在走到main之前享郊,到底還做了什么。

編譯過程&庫

在分析app啟動之前孝鹊,我們需要先了解iOS app的編譯過程以及動態(tài)庫靜態(tài)庫炊琉。

編譯過程

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

  • 源文件:載入.h又活、.m苔咪、.cpp等文件

  • 預處理:替換宏,刪除注釋柳骄,展開頭文件团赏,產生.i文件

  • 編譯:將.i文件轉換為匯編語言,產生.s文件

  • 匯編:將匯編文件轉換為機器碼文件耐薯,產生.o文件

  • 鏈接:對.o文件中引用其他庫的地方進行引用舔清,生成最后的可執(zhí)行文件

    image

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

  • 靜態(tài)庫:在鏈接階段,會將可匯編生成的目標程序與引用的庫一起鏈接打包到可執(zhí)行文件當中曲初。此時的靜態(tài)庫就不會在改變了体谒,因為它是編譯時被直接拷貝一份,復制到目標程序里的

    • 好處:編譯完成后复斥,庫文件實際上就沒有作用了营密,目標程序沒有外部依賴,直接就可以運行

    • 缺點:由于靜態(tài)庫會有兩份目锭,所以會導致目標程序的體積增大评汰,對內存纷捞、性能、速度消耗很大

  • 動態(tài)庫:程序編譯時并不會鏈接到目標程序中被去,目標程序只會存儲指向動態(tài)庫的引用主儡,在程序運行時才被載入

    • 優(yōu)勢

      • 減少打包之后app的大小:因為不需要拷貝至目標程序中,所以不會影響目標程序的體積惨缆,與靜態(tài)庫相比糜值,減少了app的體積大小

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

      • 通過更新動態(tài)庫坯墨,達到更新程序的目的:由于運行時才載入的特性寂汇,可以隨時對庫進行替換,而不需要重新編譯代碼

    • 缺點:動態(tài)載入會帶來一部分性能損失捣染,使用動態(tài)庫也會使得程序依賴于外部環(huán)境骄瓣,如果環(huán)境缺少了動態(tài)庫,或者庫的版本不正確耍攘,就會導致程序無法運行

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

image

dyld加載流程分析

根據dyld源碼榕栏,以及libobjclibSystem蕾各、libdispatch源碼協同分析

什么是dyld扒磁?

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

所以 App的啟動流程圖如下

image

app啟動的起始點

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

    image

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

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

image

dyld::_main函數源碼分析

  • dyld-750.6源碼中查找_dyld_start,查找arm64架構發(fā)現,是由匯編實現起暮,通過匯編注釋發(fā)現會調用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)方法卖氨,是一個C++方法(以arm64架構為例)

    image

源碼中搜索dyldbootstrap找到命名作用空間,再在這個文件中查找start方法负懦,其核心是返回值的調用了dyldmain函數筒捺,其中macho_headerMach-O的頭部,而dyld加載的文件就是Mach-O類型的纸厉,即Mach-O類型是可執(zhí)行文件類型系吭,由四部分組成:Mach-O頭部、Load Command颗品、section肯尺、Other Data沃缘,可以通過MachOView查看可執(zhí)行文件信息

    • image

進入dyld::_main的源碼實現,特別長则吟,大約600多行槐臀,如果對dyld加載流程不太了解的童鞋,可以根據_main函數的返回值進行反推氓仲,這里就多作說明水慨。在_main函數中主要做了一下幾件事情:

  • 【第一步:環(huán)境變量配置】:根據環(huán)境變量設置相應的值以及獲取當前運行架構

    image
  • 【第二步:共享緩存】:檢查是否開啟了共享緩存,以及共享緩存是否映射到共享區(qū)域敬扛,例如UIKit晰洒、CoreFoundation

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

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

image

第三步:主程序初始化

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

    image

進入instantiateFromLoadedImage源碼,其中創(chuàng)建一個ImageLoader實例對象缆巧,通過instantiateMainExecutable方法創(chuàng)建

    • image

進入instantiateMainExecutable源碼布持,其作用是為主可執(zhí)行文件創(chuàng)建映像,返回一個ImageLoader類型的image對象陕悬,即主程序题暖。其中sniffLoadCommands函數時獲取Mach-O類型文件Load Command的相關信息,并對其進行各種校驗

    • image

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

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

    image
  • runInitializers其核心代碼是processInitializers函數的調用進入processInitializers函數的源碼實現胧卤,其中對鏡像列表調用recursiveInitialization函數進行遞歸實例化

    • image
  • 全局搜索recursiveInitialization(函數,其源碼實現如下

    • image

在這里,需要分成兩部分探索拼岳,一部分是notifySingle函數枝誊,一部分是doInitialization函數,首先探索notifySingle函數

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

    image

全局搜索sNotifyObjCInit惜纸,發(fā)現沒有找到實現叶撒,有賦值操作

    • image

搜索registerObjCNotifiers在哪里調用了,發(fā)現在_dyld_objc_notify_register進行了調用

    • image

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

objc4-781源碼中搜索_dyld_objc_notify_register耐版,發(fā)現在_objc_init源碼中調用了該方法祠够,并傳入了參數,所以sNotifyObjCInit賦值的就是objc中的load_images粪牲,而load_images會調用所有的+load方法古瓤。所以綜上所述,notifySingle是一個回調函數

image

load函數加載

下面我們進入load_images的源碼看看其實現,以此來證明load_images中調用了所有的load函數

  • 通過objc源碼中_objc_init源碼實現落君,進入load_images的源碼實現

    image

進入call_load_methods源碼實現穿香,可以發(fā)現其核心是通過do-while循環(huán)調用+load方法

    • image

進入call_class_loads源碼實現,了解到這里調用的load方法證實我們前文提及的類的load方法

    • image

所以叽奥,load_images調用了所有的load函數扔水,以上的源碼分析過程正好對應堆棧的打印信息

    • image

【總結】load的源碼鏈為:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一個回調處理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
那么問題又來了,_objc_init是什么時候調用的呢朝氓?請接著往下看

走到objc_objc_init函數魔市,發(fā)現走不通了,我們回退到recursiveInitialization遞歸函數的源碼實現赵哲,發(fā)現我們忽略了一個函數doInitialization

    • image

進入doInitialization函數的源碼實現

    • image

這里也需要分成兩部分待德,一部分是doImageInit函數,一部分是doModInitFunctions函數

  • 進入doImageInit源碼實現枫夺,其核心主要是for循環(huán)加載方法的調用将宪,這里需要注意的一點是,libSystem的初始化必須先運行

    image

進入doModInitFunctions源碼實現橡庞,這個方法中加載了所有C++文件

    • image

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

    • image

走到這里较坛,還是沒有找到_objc_init的調用?怎么辦呢扒最?放棄嗎丑勤?當然不行,我們還可以通過_objc_init加一個符號斷點來查看調用_objc_init前的堆棧信息吧趣,

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

    image

libsystem中查找libSystem_initializer强挫,查看其中的實現

    • image

根據前面的堆棧信息岔霸,我們發(fā)現走的是libSystem_initializer中會調用libdispatch_init函數,而這個函數的源碼是在libdispatch開源庫中的俯渤,在libdispatch中搜索libdispatch_init

    • image

進入_os_object_init源碼實現呆细,其源碼實現調用了_objc_init函數

    • image

結合上面的分析,從初始化_objc_init注冊的_dyld_objc_notify_register的參數2稠诲,即load_images侦鹏,到sNotifySingle --> sNotifyObjCInie=參數2sNotifyObjcInit()調用,形成了一個閉環(huán)

所以可以簡單的理解為sNotifySingle這里是添加通知即addObserver臀叙,_objc_init中調用_dyld_objc_notify_register相當于發(fā)送通知,即push价卤,而sNotifyObjcInit相當于通知的處理函數劝萤,即selector

【總結】:_objc_init的源碼鏈:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(li#### 第九步:尋找主入口函數

  • 匯編調試,可以看到顯示來到+[ViewController load]方法

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

第九步:尋找主入口函數

  • 匯編調試慎璧,可以看到顯示來到+[ViewController load]方法

    image

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

  • image

點擊stepover,繼續(xù)往下跨释,跑完了整個流程,會回到_dyld_start,然后調用main()函數,通過匯編完成main的參數賦值等操作

    • image

dyld匯編源碼實現

    • dyld中main部分的匯編源碼實現

注意:main是寫定的函數厌处,寫入內存鳖谈,讀取到dyld,如果修改了main函數的名稱阔涉,會報錯

    • image

所以缆娃,綜上所述,最終dyld加載流程瑰排,如下圖所示贯要,圖中也詮釋了前文中的問題:為什么是load-->Cxx-->main的調用順序

    • dyld加載流程
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市椭住,隨后出現的幾起案子崇渗,更是在濱河造成了極大的恐慌,老刑警劉巖京郑,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宅广,死亡現場離奇詭異,居然都是意外死亡些举,警方通過查閱死者的電腦和手機跟狱,發(fā)現死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來金拒,“玉大人兽肤,你說我怎么就攤上這事⌒髋祝” “怎么了资铡?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長幢码。 經常有香客問我笤休,道長,這世上最難降的妖魔是什么症副? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任店雅,我火速辦了婚禮,結果婚禮上贞铣,老公的妹妹穿的比我還像新娘闹啦。我一直安慰自己,他們只是感情好辕坝,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布窍奋。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琳袄。 梳的紋絲不亂的頭發(fā)上江场,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機與錄音窖逗,去河邊找鬼址否。 笑死,一個胖子當著我的面吹牛碎紊,可吹牛的內容都是我干的佑附。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼矮慕,長吁一口氣:“原來是場噩夢啊……” “哼帮匾!你這毒婦竟也來了?” 一聲冷哼從身側響起痴鳄,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤瘟斜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后痪寻,有當地人在樹林里發(fā)現了一具尸體螺句,經...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年橡类,在試婚紗的時候發(fā)現自己被綠了蛇尚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡顾画,死狀恐怖取劫,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情研侣,我是刑警寧澤谱邪,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站庶诡,受9級特大地震影響惦银,放射性物質發(fā)生泄漏。R本人自食惡果不足惜末誓,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一扯俱、第九天 我趴在偏房一處隱蔽的房頂上張望盲镶。 院中可真熱鬧亮曹,春花似錦兔院、人聲如沸杜秸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膏孟。三九已至鹃锈,卻和暖如春窜醉,著一層夾襖步出監(jiān)牢的瞬間宪萄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工榨惰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拜英,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓琅催,卻偏偏與公主長得像居凶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子藤抡,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內容