實(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)備工作
- 可編譯的
objc4-781
源碼: http://www.reibang.com/p/45dc31d91000dyld-750.6
: https://opensource.apple.com/tarballs/dyld/libdispatch-1173.40.5
: https://opensource.apple.com/tarballs/libdispatch/Libsystem-1281
: https://opensource.apple.com/tarballs/Libsystem/
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
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
打印堆棧信息
從
bt
打印的堆棧信息
中可以看到,每一步都是dyld
在進(jìn)行調(diào)用
的
-
堆棧信息
中展示了APP啟動(dòng)
前的完整流程
羹铅。接下來(lái)我們就沿著
這個(gè)流程
蚀狰,從源碼
中尋找答案愉昆。
啟動(dòng)dyld
第一步
:執(zhí)行dyld
中的_dyld_start
我們打開(kāi)dyld
源碼职员,全局搜索_dyld_start
,找到入口:
- 我們從匯編代碼中看到調(diào)用了
dyldbootstrap::start
跛溉,與我們的第二步完全吻合焊切。
第二步
:執(zhí)行dyldbootstrap::start
- 全局搜索
dyldbootstrap
,發(fā)現(xiàn)是個(gè)命名空間芳室,折疊內(nèi)部函數(shù)专肪,找到start
函數(shù):
打開(kāi)start
函數(shù),發(fā)現(xiàn)最后執(zhí)行了dyld::_main
函數(shù)堪侯,這也與我們第三步完全吻合
第三步
:執(zhí)行_main
函數(shù)
進(jìn)入main
函數(shù)嚎尤,發(fā)現(xiàn)有600多行?? ,在這里伍宦,我們可以梳理出APP啟動(dòng)的完整流程:
3.1 設(shè)置運(yùn)行環(huán)境
- 設(shè)置運(yùn)行參數(shù)芽死、環(huán)境變量,獲取當(dāng)前運(yùn)行框架
3.2 加載共享緩存
-
checkSharedRegionDisable
檢查共享緩存是否禁用后次洼,調(diào)用mapSharedCache
加載共享緩存关贵。
3.3 實(shí)例化主程序
- 將
主程序Mach-O
加載進(jìn)內(nèi)存,返回一個(gè)ImageLoader
類(lèi)型的image
對(duì)象卖毁,即主程序
3.4 加載插入的動(dòng)態(tài)庫(kù)
- 遍歷
DYLD_INSERT_LIBRARIES
環(huán)境變量揖曾,調(diào)用loadInsertedDylib
加載庫(kù)
3.5 鏈接主程序
3.6 鏈接插入的動(dòng)態(tài)庫(kù)
3.7 執(zhí)行弱符號(hào)綁定
3.8 執(zhí)行初始化方法
- 進(jìn)入
initializeMainExecutable
函數(shù)
發(fā)現(xiàn)都是調(diào)用
ImageLoader
對(duì)象的runInitializers
方法來(lái)初始化dylib
和主程序
全局搜索
runInitializers
,在ImageLoader.cpp
文件中找到實(shí)現(xiàn)函數(shù)
。
- 核心代碼為
processInitializers
函數(shù)的調(diào)用炭剪,進(jìn)入:
-
recursiveInitialization
是ImageLoader
對(duì)象的調(diào)用方法练链,全局搜索:
-
遞歸
完成了所有對(duì)象的初始化
,并將鏡像初始化進(jìn)度
實(shí)時(shí)告知
外部關(guān)聯(lián)對(duì)象念祭。
3.9 尋找main入口
以上就是完整的app啟動(dòng)流程兑宇。
這里對(duì)3.8 執(zhí)行初始化方法 最后一步的2個(gè)內(nèi)容進(jìn)行繼續(xù)探究:
-
notifySingle
如何告知外部 -
doInitialization
初始化
1. notifySingle如何告知外部
-
全局搜索
notifySingle
:
核心代碼:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())
,我們?nèi)炙阉?code>sNotifyObjCInit,發(fā)現(xiàn)沒(méi)有找到實(shí)現(xiàn)粱坤,但是有賦值操作
:
- 搜索
registerObjCNotifiers
在哪里被調(diào)用:
- 發(fā)現(xiàn)在
_dyld_objc_notify_register
中調(diào)用隶糕。而dyld_objc
需要在libobjc
源碼中搜索。 - 我們打開(kāi)
objc4
源碼站玄,搜索_dyld_objc_notify_register(
- 發(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)用
- 進(jìn)入
call_load_methods
函數(shù)绵估,核心代碼循環(huán)調(diào)用call_class_loads
函數(shù)
- 進(jìn)入
call_class_loads
函數(shù)內(nèi)部,此處明確了load
方法的調(diào)用卡骂。
- 明確
+load
方法的加載時(shí)機(jī)国裳;
- 明確只有
+load
這個(gè)名稱(chēng)才有效(因?yàn)?code>sel已固定,系統(tǒng)只檢查load
這個(gè)方法名)
對(duì)比在+load
函數(shù)斷點(diǎn)處打印的堆棧信息
全跨,與我們源碼分析過(guò)程
完全吻合
缝左。
notifySingle
從dyld
跨庫(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)作:
- 進(jìn)入
doInitialization
:
發(fā)現(xiàn)有doImageInit
和doModInitFunctions
2個(gè)初始化操作
-
doImageInit
函數(shù)少办,for循環(huán)
實(shí)現(xiàn)鏡像的初始化(macho內(nèi)獲取地址和偏移值,拿到初始化函數(shù))诵原,libSystem
系統(tǒng)庫(kù)的初始化優(yōu)先級(jí)較高英妓。
-
doModInitFunctions
函數(shù): 該函數(shù)內(nèi)實(shí)現(xiàn)了所有Cxx
文件:
在測(cè)試代碼的c++
構(gòu)造函數(shù)constructor
處加入斷點(diǎn)
挽放,bt
打印堆棧信息檢驗(yàn)
,確實(shí)是在doModInitFunctions
函數(shù)內(nèi)完成了實(shí)現(xiàn)蔓纠。
探索_objc_init
調(diào)用時(shí)機(jī)
在objc4
源碼中搜索_objc_init
祟剔,加入斷點(diǎn)
涩拙,運(yùn)行測(cè)試代碼。
- 發(fā)現(xiàn)也是在
doModInitFunctions
函數(shù)后,調(diào)用了libSystem
庫(kù)的initializer
方法改衩。
驗(yàn)證流程:
- 打開(kāi)
libSystem
源代碼侣集,搜索libSystem_initializer
:
- 進(jìn)入
libdispatch_init
谣殊,發(fā)現(xiàn)什么在libdispatch.dylib庫(kù)中實(shí)現(xiàn)澄惊。
打開(kāi)
libdispatch
源碼,搜索libdispatch_init
:
發(fā)現(xiàn)調(diào)用了
os_object_init
硬贯,搜索_os_object_init
:在此處調(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)烈建議閱讀以下官方資源:
- 快速熟悉Mach-O結(jié)構(gòu)(后續(xù)有變動(dòng))
- dyld如何將mach-o信息映射到內(nèi)存中
- app啟動(dòng)流程(舊版)和優(yōu)化建議
- 介紹dyld歷史饼煞,引出dyld3(圍繞性能源葫、安全、占用資源進(jìn)行優(yōu)化)
- 介紹App Launch工具砖瞧,優(yōu)化啟動(dòng)時(shí)間
本文僅簡(jiǎn)單記錄dyld的大致啟動(dòng)流程息堂,部分細(xì)節(jié)并未展開(kāi)拓展。源碼的探索之旅繼續(xù)進(jìn)行...