本文的目的主要是分析dyld
的加載流程慢逾,了解在main函數之前边灭,底層還做了什么
引子
-
創(chuàng)建一個project煞额,在
ViewController
中重寫了load
方法波闹,在main
中加了一個C++
方法一喘,即kcFUnc
逸贾,請問它們的打印先后順序是什么? 運行程序津滞,查看 load铝侵、kcFunc、main的
打印順序
触徐,下面是打印結果咪鲜,通過結果可以看出其順序是load --> C++方法 --> main
為什么是這么一個順序?按照常規(guī)的思維理解撞鹉,main不是入口函數嗎疟丙?為什么不是main最先執(zhí)行?
下面根據這個問題鸟雏,我們來探索在走到main之前享郊,到底還做了什么。
編譯過程&庫
在分析app啟動之前孝鹊,我們需要先了解iOS app的編譯過程
以及動態(tài)庫
和靜態(tài)庫
炊琉。
編譯過程
其中編譯過程
如下圖所示,主要分為以下幾步:
源文件
:載入.h又活、.m苔咪、.cpp等文件預處理
:替換宏,刪除注釋柳骄,展開頭文件团赏,產生.i文件編譯
:將.i文件轉換為匯編語言,產生.s文件匯編
:將匯編文件轉換為機器碼文件耐薯,產生.o文件-
鏈接
:對.o文件中引用其他庫的地方進行引用舔清,生成最后的可執(zhí)行文件
靜態(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)庫的圖示如圖所示
dyld加載流程分析
根據dyld
源碼榕栏,以及libobjc
、libSystem
蕾各、libdispatch
源碼協同分析
什么是dyld扒磁?
dyld
(the dynamic link editor)是蘋果的動態(tài)鏈接器
,是蘋果操作系統的重要組成部分式曲,在app被編譯打包成可執(zhí)行文件格式的Mach-O
文件后妨托,交由dyld負責連接,加載程序
所以 App的啟動流程圖如下
app啟動的起始點
-
在前文的demo中检访,在
load
方法處加一個斷點
始鱼,通過bt
堆棧信息查看app啟動是從哪里開始的
【app啟動起點】:通過程序運行發(fā)現,是從
dyld
中的_dyld_start
開始的脆贵,所以需要去OpenSource下載一份dyld的源碼來進行分析
也可以通過xcode左側的堆棧信息來找到入口
dyld::_main函數源碼分析
-
在
dyld-750.6
源碼中查找_dyld_start
,查找arm64架構
發(fā)現,是由匯編實現起暮,通過匯編注釋發(fā)現會調用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
方法卖氨,是一個C++
方法(以arm64架構為例)
源碼中搜索dyldbootstrap
找到命名作用空間
,再在這個文件中查找start
方法负懦,其核心是返回值的調用了dyld
的main
函數筒捺,其中macho_header
是Mach-O
的頭部,而dyld
加載的文件就是Mach-O類型
的纸厉,即Mach-O類型是可執(zhí)行文件類型
系吭,由四部分組成:Mach-O頭部、Load Command颗品、section肯尺、Other Data
沃缘,可以通過MachOView
查看可執(zhí)行文件信息
進入dyld::_main
的源碼實現,特別長则吟,大約600多行槐臀,如果對dyld加載流程不太了解的童鞋,可以根據_main
函數的返回值進行反推氓仲,這里就多作說明水慨。在_main函數中主要做了一下幾件事情:
-
【第一步:
環(huán)境變量配置
】:根據環(huán)境變量設置相應的值以及獲取當前運行架構 【第二步:
共享緩存
】:檢查是否開啟了共享緩存,以及共享緩存是否映射到共享區(qū)域敬扛,例如UIKit
晰洒、CoreFoundation
等
- 【第三步:
主程序的初始化
】:調用instantiateFromLoadedImage
函數實例化了一個ImageLoader
對象
- 【第四步:
插入動態(tài)庫
】:遍歷DYLD_INSERT_LIBRARIES
環(huán)境變量,調用loadInsertedDylib
加載
- 【第五步:
link 主程序
】
- 【第六步:
link 動態(tài)庫
】
- 【第七步:
弱符號綁定
】
- 【第八步:
執(zhí)行初始化方法
】
【第九步:尋找主程序入口
即main
函數】:從Load Command
讀取LC_MAIN
入口啥箭,如果沒有欢顷,就讀取LC_UNIXTHREAD
,這樣就來到了日常開發(fā)中熟悉的main
函數了
第三步:主程序初始化
-
sMainExecutable
表示主程序變量捉蚤,查看其賦值抬驴,是通過instantiateFromLoadedImage
方法初始化
進入instantiateFromLoadedImage
源碼,其中創(chuàng)建一個ImageLoader
實例對象缆巧,通過instantiateMainExecutable
方法創(chuàng)建
進入instantiateMainExecutable
源碼布持,其作用是為主可執(zhí)行文件創(chuàng)建映像,返回一個ImageLoader
類型的image對象陕悬,即主程序
题暖。其中sniffLoadCommands
函數時獲取Mach-O類型文件
的Load Command
的相關信息,并對其進行各種校驗
第八步:執(zhí)行初始化方法
-
進入
initializeMainExecutable
源碼,主要是循環(huán)遍歷
捉超,都會執(zhí)行runInitializers
方法 runInitializers
其核心代碼是processInitializers
函數的調用進入processInitializers
函數的源碼實現胧卤,其中對鏡像列表調用recursiveInitialization
函數進行遞歸實例化全局搜索
recursiveInitialization(
函數,其源碼實現如下
在這里,需要分成兩部分探索拼岳,一部分是notifySingle
函數枝誊,一部分是doInitialization
函數,首先探索notifySingle
函數
notifySingle 函數
-
全局搜索
notifySingle(
函數,其重點是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
這句
全局搜索sNotifyObjCInit
惜纸,發(fā)現沒有找到實現叶撒,有賦值操作
搜索registerObjCNotifiers
在哪里調用了,發(fā)現在_dyld_objc_notify_register
進行了調用
注意:_dyld_objc_notify_register
的函數需要在libobjc
源碼中搜索
在objc4-781
源碼中搜索_dyld_objc_notify_register
耐版,發(fā)現在_objc_init
源碼中調用了該方法祠够,并傳入了參數,所以sNotifyObjCInit
的賦值
的就是objc
中的load_images
粪牲,而load_images
會調用所有的+load
方法古瓤。所以綜上所述,notifySingle
是一個回調函數
load函數加載
下面我們進入load_images
的源碼看看其實現,以此來證明load_images
中調用了所有的load
函數
-
通過objc源碼中_objc_init源碼實現落君,進入
load_images
的源碼實現
進入call_load_methods
源碼實現穿香,可以發(fā)現其核心是通過do-while
循環(huán)調用+load
方法
進入call_class_loads
源碼實現,了解到這里調用的load
方法證實我們前文提及的類的load
方法
所以叽奥,load_images
調用了所有的load
函數扔水,以上的源碼分析過程正好對應堆棧的打印信息
【總結】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
進入doInitialization
函數的源碼實現
這里也需要分成兩部分待德,一部分是doImageInit
函數,一部分是doModInitFunctions
函數
-
進入
doImageInit
源碼實現枫夺,其核心主要是for循環(huán)加載方法的調用
将宪,這里需要注意的一點是,libSystem
的初始化必須先運行
進入doModInitFunctions
源碼實現橡庞,這個方法中加載了所有C++
文件
可以通過測試程序的堆棧信息來驗證,在C++方法處加一個斷點
走到這里较坛,還是沒有找到_objc_init的調用?怎么辦呢扒最?放棄嗎丑勤?當然不行,我們還可以通過_objc_init
加一個符號斷點來查看調用_objc_init前的堆棧信息吧趣,
-
_objc_init
加一個符號斷點法竞,運行程序,查看_objc_init
斷住后的堆棧信息
在libsystem
中查找libSystem_initializer
强挫,查看其中的實現
根據前面的堆棧信息岔霸,我們發(fā)現走的是libSystem_initializer
中會調用libdispatch_init
函數,而這個函數的源碼是在libdispatch
開源庫中的俯渤,在libdispatch
中搜索libdispatch_init
進入_os_object_init
源碼實現呆细,其源碼實現調用了_objc_init
函數
結合上面的分析,從初始化_objc_init
注冊的_dyld_objc_notify_register
的參數2稠诲,即load_images
侦鹏,到sNotifySingle
--> sNotifyObjCInie=參數2
到sNotifyObjcInit()
調用,形成了一個閉環(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]
方法
繼續(xù)執(zhí)行床嫌,來到kcFunc
的C++函數
點擊stepover
,繼續(xù)往下跨释,跑完了整個流程,會回到_dyld_start
,然后調用main()
函數,通過匯編完成main
的參數賦值等操作
dyld
匯編源碼實現
注意:main
是寫定的函數厌处,寫入內存鳖谈,讀取到dyld
,如果修改了main函數的名稱
阔涉,會報錯
所以缆娃,綜上所述,最終dyld加載流程
瑰排,如下圖所示贯要,圖中也詮釋了前文中的問題:為什么是load-->Cxx-->main
的調用順序