OC底層原理十五:dyld 應(yīng)用程序加載

OC底層原理 學(xué)習(xí)大綱

實(shí)際開(kāi)發(fā)中卧抗,大部分人都只知道main是程序的入口。但是app在啟動(dòng)前鳖粟,具體做了哪些事情社裆,如何保證進(jìn)入main函數(shù)時(shí),所有資源都準(zhǔn)備好了向图?+(void)load函數(shù)為何能幫你把一些自定義事項(xiàng)在啟動(dòng)前就處理好泳秀?

如果你也有這些疑問(wèn),那本節(jié)榄攀,我們一起探索應(yīng)用程序的整個(gè)啟動(dòng)加載過(guò)程嗜傅。

1. 檢查main、load檩赢、C++(constructor) 的執(zhí)行順序
2. 靜態(tài)庫(kù)與動(dòng)態(tài)庫(kù)
3. app啟動(dòng)加載過(guò)程

準(zhǔn)備工作

1. main吕嘀、load、C++ 的執(zhí)行順序

  • 測(cè)試代碼:
__attribute__((constructor)) void htFunc() {
    printf("%s \n",__func__);
}

@interface HTPerson : NSObject
@end

@implementation HTPerson

+ (void)load {
    NSLog(@"%s", __func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%s",__func__);
    }
    return 0;
}
  • 打印順序: load -> c++(constructor) -> main
image.png
  • main函數(shù)作為程序入口贞瞒,為什么是最后執(zhí)行呢偶房?

帶著這個(gè)疑問(wèn),我們往下學(xué)習(xí)军浆。

2. 靜態(tài)庫(kù)與動(dòng)態(tài)庫(kù)

代碼庫(kù)有靜態(tài)庫(kù)動(dòng)態(tài)庫(kù)兩種棕洋,在開(kāi)始探索app啟動(dòng)流程前,我們先了解兩者的區(qū)別乒融。

2.1 靜態(tài)庫(kù):

靜態(tài)編譯的庫(kù)掰盘,在編譯時(shí)就將整個(gè)函數(shù)庫(kù)的所有數(shù)據(jù)都整合進(jìn)目標(biāo)代碼中。尾綴有.a赞季、.lib愧捕、.framework等。

  • 優(yōu)點(diǎn): 模塊化申钩,分工合作晃财,提高了代碼的復(fù)用和核心技術(shù)的保密程度
  • 缺點(diǎn): 會(huì)加大包的體積。如果靜態(tài)函數(shù)庫(kù)被改變,程序必須重新編譯断盛。

2.2 動(dòng)態(tài)庫(kù):

編譯時(shí)不會(huì)將函數(shù)庫(kù)編譯進(jìn)目標(biāo)代碼中罗洗,只有程序執(zhí)行相關(guān)函數(shù)時(shí),才調(diào)用函數(shù)庫(kù)的相應(yīng)函數(shù)钢猛。尾綴有.tbd伙菜、.so.framework

  • 優(yōu)點(diǎn): 可執(zhí)行文件體積小命迈,多個(gè)應(yīng)用程序共享內(nèi)存中同一份庫(kù)文件贩绕,節(jié)省內(nèi)存資源,支持實(shí)時(shí)模塊升級(jí)壶愤。

蘋(píng)果的動(dòng)態(tài)庫(kù)支持所有APP共享內(nèi)存(如UIKit)淑倾,但APP動(dòng)態(tài)庫(kù)是寫(xiě)入app main bundle根目錄中,運(yùn)行在沙盒中征椒,只支持當(dāng)前APP內(nèi)共享內(nèi)存 娇哆。(iOS8后App Extension功能支持主app和插件之間共享動(dòng)態(tài)庫(kù))

3. App加載過(guò)程

我們直觀(guān)感受的App加載過(guò)程是:源文件(.h .m .cpp)-> 預(yù)編譯(詞法語(yǔ)法分析) -> 編譯(載入靜態(tài)庫(kù)) -> 匯編 -> 鏈接(關(guān)聯(lián)動(dòng)態(tài)庫(kù)) -> 生成可執(zhí)行文件(mach-o)

作為程序員,我們知道代碼是“死”的勃救,只有當(dāng)觸發(fā)啟動(dòng)碍讨,按照我們設(shè)計(jì)好的流程一步步執(zhí)行,才能讓程序“活”起來(lái)蒙秒。

程序啟動(dòng)過(guò)程中勃黍,當(dāng)系統(tǒng)內(nèi)核資源準(zhǔn)備好后,dyld動(dòng)態(tài)鏈接器就承擔(dān)著管理者的角色:

配置應(yīng)用環(huán)境->初始化主程序->加載共享緩存->加載動(dòng)態(tài)庫(kù)->鏈接主程序->鏈接動(dòng)態(tài)庫(kù)->弱符號(hào)綁定->執(zhí)行初始化->調(diào)用main函數(shù)晕讲。

到了main函數(shù)后覆获,就交給程序員們自由發(fā)揮了。

dyld全稱(chēng)the dynamic link editor瓢省,動(dòng)態(tài)鏈接器锻梳。是蘋(píng)果操作系統(tǒng)的一個(gè)重要組成部分。在iOS/Mac OSX系統(tǒng)中净捅,僅有很少量的進(jìn)程只需要內(nèi)核就能完成加載疑枯,基本上所有進(jìn)程都是動(dòng)態(tài)鏈接的,所以mach-o鏡像文件中會(huì)有很多外部庫(kù)和符號(hào)的引用蛔六,但這些引用并不能直接用荆永,在啟動(dòng)時(shí)還需要通過(guò)這些引用進(jìn)行內(nèi)容的填補(bǔ),這個(gè)填補(bǔ)工作就是dyld動(dòng)態(tài)鏈接器來(lái)完成的国章,也就是符號(hào)綁定具钥。dyld動(dòng)態(tài)鏈接器在系統(tǒng)中是以一個(gè)用戶(hù)態(tài)的可執(zhí)行文件存在,一般應(yīng)用程序會(huì)在Mach-o文件部分指定一個(gè)LC_KIAD_DYLINKER的加載命令液兽,此加載命令指定了dyld的路徑骂删,通常它的默認(rèn)值是/usr/lib/dyld掌动。系統(tǒng)內(nèi)核在加載Mack-o文件時(shí),都需要用dyld(位于/usr/lib/dyld)程序進(jìn)行鏈接宁玫。

共享緩存機(jī)制

在iOS生態(tài)中粗恢,每個(gè)程序都會(huì)用到大量系統(tǒng)庫(kù),但如果我們每個(gè)程序運(yùn)行時(shí)欧瘪,都獨(dú)立加載其依賴(lài)的相關(guān)動(dòng)態(tài)庫(kù)眷射,勢(shì)必會(huì)造成運(yùn)行緩慢。為了優(yōu)化啟動(dòng)速度程序性能佛掖,共享緩存機(jī)制應(yīng)運(yùn)而生妖碉。所有默認(rèn)的動(dòng)態(tài)鏈接庫(kù)被合并成一個(gè)大的緩存文件,按不同架構(gòu)分別保存芥被。

本節(jié)主要是梳理驗(yàn)證APP啟動(dòng)的完整流程欧宜。具體內(nèi)部細(xì)節(jié)使用法決,后續(xù)在其他文章中進(jìn)行拓展拴魄。

  • 我們?cè)?code>load函數(shù)內(nèi)部打斷點(diǎn)冗茸,bt打印堆棧信息
image.png

bt打印的堆棧信息中可以看到,每一步都是dyld在進(jìn)行調(diào)用

  • 堆棧信息中展示了APP啟動(dòng)前的完整流程羹铅。接下來(lái)我們就沿著這個(gè)流程蚀狰,從源碼中尋找答案愉昆。

啟動(dòng)dyld

第一步:執(zhí)行dyld中的_dyld_start

我們打開(kāi)dyld源碼职员,全局搜索_dyld_start,找到入口:

image.png
  • 我們從匯編代碼中看到調(diào)用了dyldbootstrap::start跛溉,與我們的第二步完全吻合焊切。

第二步:執(zhí)行dyldbootstrap::start

  • 全局搜索dyldbootstrap,發(fā)現(xiàn)是個(gè)命名空間芳室,折疊內(nèi)部函數(shù)专肪,找到start函數(shù):
image.png

打開(kāi)start函數(shù),發(fā)現(xiàn)最后執(zhí)行了dyld::_main函數(shù)堪侯,這也與我們第三步完全吻合

image.png

第三步:執(zhí)行_main函數(shù)

進(jìn)入main函數(shù)嚎尤,發(fā)現(xiàn)有600多行?? ,在這里伍宦,我們可以梳理出APP啟動(dòng)的完整流程:

image.png
3.1 設(shè)置運(yùn)行環(huán)境
  • 設(shè)置運(yùn)行參數(shù)芽死、環(huán)境變量,獲取當(dāng)前運(yùn)行框架
image.png
3.2 加載共享緩存
  • checkSharedRegionDisable檢查共享緩存是否禁用后次洼,調(diào)用mapSharedCache加載共享緩存关贵。
image.png
3.3 實(shí)例化主程序
  • 主程序Mach-O加載進(jìn)內(nèi)存,返回一個(gè)ImageLoader類(lèi)型的image對(duì)象卖毁,即主程序
image.png
3.4 加載插入的動(dòng)態(tài)庫(kù)
  • 遍歷DYLD_INSERT_LIBRARIES環(huán)境變量揖曾,調(diào)用loadInsertedDylib加載庫(kù)
image.png
3.5 鏈接主程序
image.png
3.6 鏈接插入的動(dòng)態(tài)庫(kù)
image.png
3.7 執(zhí)行弱符號(hào)綁定
image.png
3.8 執(zhí)行初始化方法
image.png
  • 進(jìn)入initializeMainExecutable函數(shù)
image.png
  • 發(fā)現(xiàn)都是調(diào)用ImageLoader對(duì)象的runInitializers方法來(lái)初始化dylib主程序

  • 全局搜索runInitializers,在ImageLoader.cpp文件中找到實(shí)現(xiàn)函數(shù)

image.png
  • 核心代碼為processInitializers函數(shù)的調(diào)用炭剪,進(jìn)入:
image.png
  • recursiveInitializationImageLoader對(duì)象的調(diào)用方法练链,全局搜索:
image.png
  • 遞歸完成了所有對(duì)象的初始化,并將鏡像初始化進(jìn)度實(shí)時(shí)告知外部關(guān)聯(lián)對(duì)象念祭。

3.9 尋找main入口
image.png

以上就是完整的app啟動(dòng)流程兑宇。


這里對(duì)3.8 執(zhí)行初始化方法 最后一步的2個(gè)內(nèi)容進(jìn)行繼續(xù)探究:

  • notifySingle如何告知外部
  • doInitialization初始化

1. notifySingle如何告知外部

  • 全局搜索notifySingle

    image.png

  • 核心代碼:(*sNotifyObjCInit)(image->getRealPath(), image->machHeader()),我們?nèi)炙阉?code>sNotifyObjCInit,發(fā)現(xiàn)沒(méi)有找到實(shí)現(xiàn)粱坤,但是有賦值操作

image.png
  • 搜索 registerObjCNotifiers在哪里被調(diào)用:
image.png
  • 發(fā)現(xiàn)在_dyld_objc_notify_register中調(diào)用隶糕。而dyld_objc需要在libobjc源碼中搜索。
  • 我們打開(kāi)objc4源碼站玄,搜索_dyld_objc_notify_register(
image.png
  • 發(fā)現(xiàn)在_objc_init方法中調(diào)用了_dyld_objc_notify_register方法枚驻,并傳入了入?yún)ⅲ?code>sNotifyObjCInit的賦值是objc傳入的load_images函數(shù)指針株旷。因?yàn)槿雲(yún)⑹?code>指針再登,所以notifySingle是一個(gè)回調(diào)函數(shù)

回調(diào)函數(shù)通過(guò)函數(shù)指針調(diào)用的函數(shù)
函數(shù)指針(地址)作為參數(shù)傳遞給另一個(gè)函數(shù)晾剖,當(dāng)該指針用來(lái)調(diào)用所指向的函數(shù)時(shí)锉矢,我們就說(shuō)這是回調(diào)函數(shù)
回調(diào)函數(shù)不是由該函數(shù)的實(shí)現(xiàn)方直接調(diào)用齿尽,而是在特定的事件或條件發(fā)生時(shí)由另外的一方調(diào)用的沽损,用于對(duì)該事件或條件進(jìn)行響應(yīng)

我們探索一下load_images函數(shù)內(nèi)部:

load_images函數(shù)

  • 進(jìn)入load_images函數(shù)內(nèi)部循头,核心代碼為call_load_methods的調(diào)用
image.png
  • 進(jìn)入call_load_methods函數(shù)绵估,核心代碼循環(huán)調(diào)用call_class_loads函數(shù)
image.png
  • 進(jìn)入call_class_loads函數(shù)內(nèi)部,此處明確了load方法的調(diào)用卡骂。
image.png
    1. 明確+load方法的加載時(shí)機(jī)国裳;
    1. 明確只有+load這個(gè)名稱(chēng)才有效(因?yàn)?code>sel已固定,系統(tǒng)只檢查load這個(gè)方法名)

對(duì)比在+load函數(shù)斷點(diǎn)處打印的堆棧信息全跨,與我們源碼分析過(guò)程完全吻合缝左。

image.png

notifySingledyld跨庫(kù)到objc,調(diào)用了load_images函數(shù)浓若,調(diào)用了所有+load函數(shù)

HTPerson類(lèi)Load函數(shù)被調(diào)用的完整流程

  • 程序啟動(dòng)_dyld_start
    -> 調(diào)用dyldbootstrap::start函數(shù) -> 調(diào)用dyld::_main函數(shù)
    -> 主程序初始化initializeMainExecutable -> 鏡像初始化ImageLoader::runInitializers
    -> 進(jìn)程初始化 ImageLoader::processInitializers -> 遞歸初始化ImageLoader::recursiveInitialization
    -> 消息發(fā)送dyld::notifySingle -> 跨到objc源碼庫(kù)調(diào)用load_images -> 調(diào)用+load方法

但是渺杉,_objc_init什么時(shí)候調(diào)用的呢? 我們繼續(xù)往下探索:


2. doInitialization初始化

  • 回到3.8步驟,我們理清楚了notifySingle的消息流程(調(diào)用回調(diào)函數(shù))七嫌,接下來(lái)看doInitialization初始化動(dòng)作:
image.png
  • 進(jìn)入doInitialization
image.png

發(fā)現(xiàn)有doImageInitdoModInitFunctions2個(gè)初始化操作

  • doImageInit函數(shù)少办,for循環(huán)實(shí)現(xiàn)鏡像的初始化(macho內(nèi)獲取地址和偏移值,拿到初始化函數(shù))诵原,libSystem系統(tǒng)庫(kù)的初始化優(yōu)先級(jí)較高英妓。
image.png
  • doModInitFunctions函數(shù): 該函數(shù)內(nèi)實(shí)現(xiàn)了所有Cxx文件:
image.png

在測(cè)試代碼的c++構(gòu)造函數(shù)constructor處加入斷點(diǎn)挽放,bt打印堆棧信息檢驗(yàn),確實(shí)是在doModInitFunctions函數(shù)內(nèi)完成了實(shí)現(xiàn)蔓纠。

image.png

探索_objc_init調(diào)用時(shí)機(jī)

objc4源碼中搜索_objc_init祟剔,加入斷點(diǎn)涩拙,運(yùn)行測(cè)試代碼。

image.png
  • 發(fā)現(xiàn)也是在doModInitFunctions函數(shù)后,調(diào)用了libSystem庫(kù)的initializer方法改衩。
    image.png

驗(yàn)證流程

  • 打開(kāi)libSystem源代碼侣集,搜索libSystem_initializer
image.png
  • 進(jìn)入libdispatch_init谣殊,發(fā)現(xiàn)什么在libdispatch.dylib庫(kù)中實(shí)現(xiàn)澄惊。
image.png
  • 打開(kāi)libdispatch源碼,搜索libdispatch_init:

    image.png

  • 發(fā)現(xiàn)調(diào)用了os_object_init硬贯,搜索_os_object_init

image.png

在此處調(diào)用了_objc_init焕襟。

_objc_init的完整調(diào)用流程

  • 程序啟動(dòng)_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)

此刻,回到文初的問(wèn)題饭豹,main鸵赖、load、C++ 的執(zhí)行順序拄衰?是否已非常清晰它褪。

  • load: 在 3.8 執(zhí)行初始化方法recursiveInitialization函數(shù)中,第一次調(diào)用notifySingle時(shí)完成了所有+load的調(diào)用翘悉。

  • c++: 在第一次調(diào)用notifySingle函數(shù)之后茫打,調(diào)用doInitialization函數(shù)中,完成了所有c++函數(shù)的調(diào)用和所有庫(kù)的初始化

  • main: 在 3.9 尋找main入口后镐确,開(kāi)始調(diào)用main函數(shù)包吝。

強(qiáng)烈建議閱讀以下官方資源:

  1. WWDC 2016 Optimizing App Startup Time
  • 快速熟悉Mach-O結(jié)構(gòu)(后續(xù)有變動(dòng))
  • dyld如何將mach-o信息映射到內(nèi)存中
  • app啟動(dòng)流程(舊版)和優(yōu)化建議
  1. WWDC 2017 App Startup Time: Past, Present, and Future
  • 介紹dyld歷史饼煞,引出dyld3(圍繞性能源葫、安全、占用資源進(jìn)行優(yōu)化)
  1. WWDC 2019 Optimizing App Launch
  • 介紹App Launch工具砖瞧,優(yōu)化啟動(dòng)時(shí)間

本文僅簡(jiǎn)單記錄dyld的大致啟動(dòng)流程息堂,部分細(xì)節(jié)并未展開(kāi)拓展。源碼的探索之旅繼續(xù)進(jìn)行...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末块促,一起剝皮案震驚了整個(gè)濱河市荣堰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌竭翠,老刑警劉巖振坚,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異斋扰,居然都是意外死亡渡八,警方通過(guò)查閱死者的電腦和手機(jī)啃洋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)屎鳍,“玉大人宏娄,你說(shuō)我怎么就攤上這事〈冢” “怎么了孵坚?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)窥淆。 經(jīng)常有香客問(wèn)我卖宠,道長(zhǎng),這世上最難降的妖魔是什么忧饭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任逗堵,我火速辦了婚禮,結(jié)果婚禮上眷昆,老公的妹妹穿的比我還像新娘蜒秤。我一直安慰自己,他們只是感情好亚斋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布作媚。 她就那樣靜靜地躺著,像睡著了一般帅刊。 火紅的嫁衣襯著肌膚如雪纸泡。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天赖瞒,我揣著相機(jī)與錄音女揭,去河邊找鬼。 笑死栏饮,一個(gè)胖子當(dāng)著我的面吹牛吧兔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播袍嬉,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼境蔼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了伺通?” 一聲冷哼從身側(cè)響起箍土,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎罐监,沒(méi)想到半個(gè)月后吴藻,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弓柱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年沟堡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疮鲫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弦叶,死狀恐怖俊犯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伤哺,我是刑警寧澤燕侠,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站立莉,受9級(jí)特大地震影響绢彤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蜓耻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一茫舶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧刹淌,春花似錦饶氏、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蔼卡,卻和暖如春喊崖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背雇逞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工荤懂, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塘砸。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓节仿,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親谣蠢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粟耻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345