首先拙毫,大家應(yīng)該都知道 _objc_init
函數(shù)是 OC 中類(lèi)加載比較關(guān)鍵的一個(gè)函數(shù)押逼,這個(gè)函數(shù)的調(diào)用棧如下:
那么箫踩,objc_init
這個(gè)函數(shù)是如何被調(diào)用的呢哎迄?又和 OC 中的類(lèi)加載有什么關(guān)系?類(lèi)又是如何被加載并以什么形式存在于運(yùn)行時(shí)呢蒂破?OC 中的成員變量馏谨、方法别渔、協(xié)議附迷、分類(lèi),這些都是如何實(shí)現(xiàn)的哎媚?
1. objc_init 的調(diào)用流程
從調(diào)用椑可以看到,_objc_init
起始于 doModinitFunctions
這個(gè)方法拨与。這個(gè)方法在 dyld 中稻据,因?yàn)?dyld3 都已經(jīng)在 iOS12 被全面使用了,dyld-433 仍然是 dyld2 的版本买喧,dyld-655 已經(jīng)是 dyld3 的版本了捻悯,所以這里以 dyld-655 的源碼來(lái)探索 _objc_init
的調(diào)用流程。
首先淤毛,doModinitFunctions
這個(gè)函數(shù)屬于 dyld 流程的“初始化方法調(diào)用”階段今缚。這一階段是整個(gè)流程的倒數(shù)第二步,也就是執(zhí)行 main 函數(shù)之前的階段低淡。
dyld 詳細(xì)流程見(jiàn)dyld:?jiǎn)?dòng)流程解析姓言。
doModinitFunctions
函數(shù)在 ImageLoader::recursiveInitialization
中被調(diào)用瞬项,關(guān)鍵代碼如下:
先看看 doInitialization
方法的邏輯:
bool ImageLoaderMachO::doInitialization(const LinkContext& context) {
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
很明顯,關(guān)鍵邏輯在于 doImageInit
和 doModInitFunctions
這兩個(gè)函數(shù)何荚。
doImageInit
內(nèi)部經(jīng)過(guò)邏輯主要是找出該 Image 對(duì)應(yīng)的 mach-O 文件中 LC_ROUTINES
表內(nèi)的函數(shù)進(jìn)行調(diào)用:
LC_ROUTINES
的定義可以直接在 mach-o 庫(kù)的 loader.h 中看到囱淋,如果 dyld 源碼中無(wú)法跳轉(zhuǎn),可以在自己的項(xiàng)目中 import <mach-o/loader.h>
來(lái)看到具體的內(nèi)容:
/*
* The routines command contains the address of the dynamic shared library
* initialization routine and an index into the module table for the module
* that defines the routine. Before any modules are used from the library the
* dynamic linker fully binds the module that defines the initialization routine
* and then calls it. This gets called before any module initialization
* routines (used for C++ static constructors) in the library.
*/
struct routines_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_ROUTINES */
uint32_t cmdsize; /* total size of this command */
uint32_t init_address; /* address of initialization routine */
uint32_t init_module; /* index into the module table that */
/* the init routine is defined in */
uint32_t reserved1;
uint32_t reserved2;
uint32_t reserved3;
uint32_t reserved4;
uint32_t reserved5;
uint32_t reserved6;
};
根據(jù)注釋來(lái)看餐塘,LC_ROUTINES
大概就是動(dòng)態(tài)庫(kù)在調(diào)用初始化函數(shù)之前需要被調(diào)用的函數(shù)妥衣。找了幾個(gè)動(dòng)態(tài)庫(kù),也沒(méi)有找到包含 LC_ROUTINES
這個(gè) load command 的動(dòng)態(tài)庫(kù)戒傻,暫時(shí)不深究吧~~~
緊接著称鳞,就來(lái)到了調(diào)用棧上最初的 doModInitFunctions
函數(shù)了,這個(gè)函數(shù)做了這么幾件事:
- 遞歸尋找 Load Command稠鼻,找到
S_MOD_INIT_FUNC_POINTERS
這個(gè) section 對(duì)應(yīng)的 Load Command冈止; - 根據(jù) slide 計(jì)算
S_MOD_INIT_FUNC_POINTERS
的具體位置,并且取出這個(gè)表中的函數(shù)指針候齿; - 進(jìn)行一系列判斷之后調(diào)用這些函數(shù)熙暴;
- 在函數(shù)調(diào)用前后進(jìn)行判斷,如果函數(shù)調(diào)用使得
dyld::gLibSystemHelpers
有值了慌盯,證明 libSystem 初始化完成周霉,此時(shí)將dyld::gProcessInfo->libSystemInitialized
標(biāo)志置為 true;
關(guān)鍵代碼:
簡(jiǎn)而言之:
- dyld 在動(dòng)態(tài)鏈接完成之后會(huì)執(zhí)行所有動(dòng)態(tài)庫(kù)的初始化函數(shù)亚皂,最后執(zhí)行主工程的初始化函數(shù)俱箱;
- 初始化函數(shù)需要使用
__attribute__
修飾,編譯器識(shí)別之后會(huì)存儲(chǔ)在 Mach-O 文件的__mod_init_func
中灭必; - 因?yàn)?libSystem 是一系列系統(tǒng)庫(kù)的集合狞谱,被很多動(dòng)態(tài)庫(kù)依賴,優(yōu)先級(jí)更高禁漓,libSystem 的初始化函數(shù)會(huì)在比較靠前的順序開(kāi)始執(zhí)行(不是第一)跟衅。而 objc 就被包含在這個(gè)庫(kù)中。objc 庫(kù)的初始化方法
objc_init
就是在 libSystem 的初始化函數(shù)中被調(diào)用播歼; -
objc_init
方法中包含了 OC 類(lèi)的加載邏輯伶跷;
至此,可以做個(gè)階段性總結(jié)了:
- dyld 初始化函數(shù)調(diào)用階段會(huì)去遞歸調(diào)用 image 的初始化函數(shù)秘狞;
- libSystem 庫(kù)在比較靠前的位置被調(diào)用叭莫,進(jìn)而觸發(fā)了
_objc_init
函數(shù)的調(diào)用;
2. _objc_init 方法做了什么
來(lái)看下 objc_init
方法里面的代碼吧:
void _objc_init(void) {
static bool initialized = false;
if (initialized) return;
initialized = true;
// 環(huán)境初始化相關(guān)
environ_init();
// 線程相關(guān)
tls_init();
// objc庫(kù)初始化方法調(diào)用烁试,即objc庫(kù)中被__attribute__修飾的方法
static_init();
// 暫無(wú)任何邏輯
lock_init();
// NSSetUncaughtExceptionHandler()的基礎(chǔ)
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
如上雇初,兩個(gè)點(diǎn)可以稍微關(guān)注下:
-
static_init();
方法調(diào)用了 objc 庫(kù)內(nèi)部的初始化方法。一般而言 image 的初始化方法在 dyld 的第八步中被調(diào)用廓潜,而 objc 則主動(dòng)調(diào)用了自己的初始化函數(shù)抵皱,有興趣的可以見(jiàn)后文善榛; -
exception_init();
方法內(nèi)部實(shí)現(xiàn)是 iOS 中使用NSSetUncaughtExceptionHandler()
的基礎(chǔ)。該方法可以設(shè)置 crash 后的處理邏輯呻畸,也是早起友盟移盆、bugly 等三方 crash 監(jiān)控 SDK 獲取 crash 堆棧信息的基礎(chǔ):
接著看代碼,從 objc_init()
代碼來(lái)看伤为,貌似 objc 并沒(méi)有進(jìn)行類(lèi)的加載咒循?此時(shí)就需要關(guān)注 _dyld_objc_notify_register
以及對(duì)應(yīng)的三個(gè)回調(diào)了,這個(gè)方法是怎么個(gè)邏輯绞愚?
3. dyld 和 objc 的聯(lián)系
_dyld_objc_notify_register
這個(gè)方法由 dyld 提供叙甸,定義如下:
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
注釋的大意是:
- 該方法專(zhuān)門(mén)為 objc-runtime 提供;
- 三個(gè)回調(diào)會(huì)在 image 被 mapped位衩、unmapped裆蒸、initialized 時(shí)分別被觸發(fā);
- dyld 在調(diào)用 mapped 這個(gè)回調(diào)時(shí)糖驴,會(huì)傳遞一個(gè) image 數(shù)組僚祷,這些 image 都是 objc 相關(guān)的 image;
- objc 不需要再調(diào)用
dlopen()
方法來(lái)加載或者維持這些 image贮缕。后續(xù)有新的 image 被載入時(shí)辙谜,仍然會(huì)調(diào)用 mapped 相關(guān)的回調(diào); - dyld 會(huì)在調(diào)用 image 初始化函數(shù)階段觸發(fā) init 回調(diào)感昼,而這個(gè)回調(diào)就是 objc 調(diào)用 +load 方法的時(shí)機(jī)装哆;
緊接著,一一驗(yàn)證上述的注釋定嗓。首先在 dyld 中找到這個(gè)函數(shù):
_dyld_objc_notify_register
只是一個(gè)對(duì)外包裝接口蜕琴,關(guān)鍵方法在 registerObjCNotifiers
:
根據(jù)注釋?zhuān)琩yld 會(huì)通過(guò) notifyBatchPartial
函數(shù)觸發(fā) mapped 回調(diào)。因?yàn)?mapped 的回調(diào)被綁定到了 sNotifyObjCMapped
這個(gè)指針蜕乡,所以我們看代碼時(shí)只需要關(guān)注 sNotifyObjCMapped
的調(diào)用邏輯即可奸绷,來(lái)看看這個(gè)函數(shù)的關(guān)鍵代碼
打個(gè)斷點(diǎn)來(lái)驗(yàn)證:
咦梗夸?有點(diǎn)不一樣层玲?別慌,這個(gè)是用的 iOS15 的模擬器反症,很明顯辛块,dyld4 已經(jīng)都被用上了。用 iOS12 的 iPhone7 看看:
完美铅碍,結(jié)論被完美驗(yàn)證~~~
即:
map_images()
是 objc 中類(lèi)初始化的主要函數(shù)润绵。該函數(shù)在 _objc_init()
調(diào)用 dyld 進(jìn)行回調(diào)綁定時(shí)就會(huì)通過(guò) notifyBatchPartial
被觸發(fā),進(jìn)而 objc 會(huì)對(duì)當(dāng)前所有 objc 相關(guān)的 image 進(jìn)行類(lèi)的初始化操作胞谈。
4. +load 函數(shù)的調(diào)用邏輯
+load
函數(shù)調(diào)用棧:
感覺(jué)核心在 notifySingle
這個(gè)函數(shù)尘盼,首先回到初始化函數(shù)的調(diào)用邏輯上憨愉,在recursiveInitialization
函數(shù)中對(duì) notifySingle
調(diào)用如下:
上圖可看出:
- 在初始化操作之前調(diào)用了一次 notify,根據(jù)注釋可以看出卿捎,應(yīng)該是即將初始化對(duì)應(yīng) image 的一個(gè)通知配紫;
- 初始化操作之后之后,發(fā)送了初始化完成的通知午阵;
這里的重點(diǎn)在第一次 notify 的 dyld_image_state_dependents_initialized
躺孝,來(lái)看看 notifySingle
函數(shù)中的關(guān)鍵代碼:
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
uint64_t t1 = mach_absolute_time();
uint64_t t2 = mach_absolute_time();
uint64_t timeInObjC = t1-t0;
uint64_t emptyTime = (t2-t1)*100;
if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
timingInfo->addTime(image->getShortName(), timeInObjC);
}
}
不看 time 相關(guān)的代碼,關(guān)鍵代碼邏輯就是:
- 判斷
sNotifyObjCInit
是否存在底桂; - 存在則執(zhí)行
sNotifyObjCInit
植袍,傳遞 image 的 path 和 mh_header 的地址;
那么 sNotifyObjCInit
是個(gè)啥籽懦?全局搜一下找到:
而 registerObjCNotifiers
又是啥呢于个?繼續(xù)全局搜索:
很明顯,又是 _dyld_objc_notify_register
函數(shù)暮顺,這個(gè)函數(shù)注冊(cè)了三個(gè)回調(diào)览濒,load_image
就是在 image 的初始化函數(shù)即將被調(diào)用之前會(huì)被觸發(fā)的回調(diào)。
其實(shí) fishhook 也是用到了該文件下的 Api拖云,只不過(guò)是
_dyld_register_func_for_add_image
函數(shù)贷笛,該方法是添加 image 相關(guān)的回調(diào),大概邏輯有點(diǎn)類(lèi)似宙项,具體就不贅述了乏苦,詳見(jiàn):iOS逆向:fishhook原理分析;
總結(jié)下邏輯:
- objc 在初始化函數(shù)
_objc_init
調(diào)用 dyld 的 Api 設(shè)置了依賴庫(kù)被加載時(shí)的回調(diào)尤筐; - 依賴庫(kù)即將被調(diào)用初始化方法時(shí)汇荐,通過(guò)通知觸發(fā)回調(diào);
- 回調(diào)執(zhí)行預(yù)先設(shè)置的函數(shù)盆繁,也就是 objc 中的
load_images
函數(shù)掀淘; -
load_images
函數(shù)執(zhí)行 objc 的類(lèi)加載的邏輯,觸發(fā) +load 方法的調(diào)用油昂;
另外革娄,需要關(guān)注一點(diǎn):notifySingle()
函數(shù)的觸發(fā)時(shí)在初始化函數(shù)調(diào)用之前,也就是說(shuō)冕碟,必須在所有依賴庫(kù)的初始化函數(shù)執(zhí)行 之前 (也就是兩個(gè)通知的前者)進(jìn)行 objc 的 +load
邏輯拦惋。
實(shí)現(xiàn)了
+load
方法的類(lèi)會(huì)被添加到費(fèi)懶加載類(lèi),在 map 的回調(diào)中就會(huì)調(diào)用realizeClassWithoutSwift
進(jìn)行加載和初始化安寺。在+load
調(diào)用之前厕妖,為了防止遺漏,仍然會(huì)進(jìn)行一次realizeClassWithoutSwift
的調(diào)用挑庶。另外言秸,+load
方法最初的設(shè)計(jì)目的是什么软能?
5. initialize 方法調(diào)用流程
詳見(jiàn):徹底搞懂+load和+initialize
6. 補(bǔ)充:objc 自己調(diào)用初始化函數(shù)
比較好玩的一點(diǎn)是:objc 庫(kù)中的初始化函數(shù)是 objc 自己調(diào)用的,而不是 dyld举畸。
這里首先要從我們經(jīng)常涉及到的 objc_init
來(lái)說(shuō)起:
該方法通過(guò) static_init
方法完成了 objc 庫(kù)中初始化方法的調(diào)用:
看看 getLibobjcInitializers
是個(gè)啥埋嵌?
本質(zhì)上是常見(jiàn)的 GETSECT
方法,但是這里最關(guān)鍵的是 __objc_init_func
俱恶。objc 用這個(gè)標(biāo)記來(lái)在 mach-O 文件中來(lái)標(biāo)識(shí) objc 獨(dú)有的初始化函數(shù)雹嗦。
但是,初始化方法不是一般都存放在 __mod_init
這個(gè) section 中嗎合是? dyld 內(nèi)部也是這個(gè)邏輯:
這個(gè) S_MOD_INIT_FUNC_POINTERS
在 mach-o 相關(guān)的源碼中:
實(shí)際測(cè)試結(jié)果:
結(jié)論:dyld 通過(guò) mach-O 文件中的 __mod_init
這個(gè) section 來(lái)記錄并調(diào)用初始化函數(shù)了罪;
看到這里會(huì)想當(dāng)疑惑,難道 objc 的初始化方法不是 dyld 加載的聪全?繼續(xù)查找 objc 源碼泊藕,看看 objc 對(duì)這個(gè) __objc_init_func
做了什么? 在 markgc 的 main 函數(shù)中做了這么一個(gè)操作:
markgc 是個(gè)啥难礼?猜測(cè)是個(gè)腳本之類(lèi)的東西娃圆?markgc 的 main 函數(shù)最終會(huì)觸發(fā)這個(gè) dosect 方法。也就是說(shuō) markgc 這個(gè)程序在 objc 的 mach-O 文件生成之后(可以理解成被編譯成了動(dòng)態(tài)庫(kù)之后)蛾茉,手動(dòng)修改了初始化方法對(duì)應(yīng)的 sectionName 和 type讼呢。
而 dyld 調(diào)用初始化方法是通過(guò) mach-O 文件中的 __mod_init
這個(gè) section 來(lái)完成調(diào)用的。objc 做了這么個(gè)騷操作之后谦炬, dyld 就不會(huì)(沒(méi)辦法悦屏,因?yàn)檎也坏綄?duì)應(yīng)的 section)在初始化階段(倒數(shù)第二步,即調(diào)用 main 函數(shù)之前)去調(diào)用這些初始化函數(shù)了键思。
按照 Apple 給出的理由是础爬,dyld 調(diào)用 objc 的初始化函數(shù)的時(shí)機(jī)太晚,主要是晚于 libc 的調(diào)用:
libc calls _objc_init() before dyld would call our static constructors, so we have to do it ourselves
總結(jié):
- libc 可能被包裝在了 libSystem 中吼鳞,而 libc 需要調(diào)用 objc看蚜,且這個(gè)調(diào)用發(fā)生在 dyld 調(diào)用 objc 初始化函數(shù)之前,所以 objc 需要自己來(lái)調(diào)用初始化函數(shù)赔桌;
- objc 通過(guò) markgc 程序?qū)?
__mod_init
修改為__objc_init_function
供炎,從而適配自己的static_init
邏輯,同時(shí)也避免了 dyld 對(duì) objc 初始化函數(shù)的重復(fù)調(diào)用纬乍;
7. 一個(gè)疑問(wèn)
如上圖碱茁,斷點(diǎn)打在 ImageLoaderMachO::doModInitFunctions
時(shí),libSystem 顯然不是第一個(gè) image仿贬,此時(shí)就有個(gè)疑問(wèn):
- 為什么自己嵌入的動(dòng)態(tài)庫(kù)
UPBase
那么靠前? - 如果按照這個(gè) image list 順序進(jìn)行初始化調(diào)用墓贿,那么 UPBase 被初始化時(shí)肯定 libSystem 還沒(méi)有初始化茧泪。雖然
map_images()
在后續(xù)被調(diào)用時(shí)會(huì)遍歷所有 image蜓氨,但是如果是涉及到 UPBase 中有初始化函數(shù)調(diào)用,那么此時(shí) objc 仍然沒(méi)有初始化的队伟,這樣會(huì)不會(huì)有問(wèn)題穴吹? - 如果沒(méi)問(wèn)題,那邏輯是怎樣的呢嗜侮? dyld 是進(jìn)行了 image 順序調(diào)整港令,類(lèi)似于依賴層級(jí)調(diào)整?或者說(shuō) image list 指令打印的不是當(dāng)前 dyld 中的 image list 中的順序锈颗?
解釋?zhuān)褐鞴こ?image 雖然在第一個(gè)顷霹,但是給到 objc 的 image 數(shù)組進(jìn)行了順序調(diào)整,且給過(guò)去之后 objc 是逆序讀取 image击吱,所以 objc 相關(guān)的 libSystem 庫(kù)比較靠前淋淀,優(yōu)先加載。
可以通過(guò)在 map_image_nolock
方法中打斷點(diǎn)覆醇,然后查看寄存區(qū)朵纷,獲取入?yún)ⅲㄟ^(guò)打印入?yún)⒌姆绞讲榭?image list 的排序:
因?yàn)樽址嵌鄠€(gè)字符加上 \0
組成永脓,所以字符串本身就是一個(gè)數(shù)組袍辞,使用 char *
表示。而 path 是字符串?dāng)?shù)組常摧,所以指向一個(gè)字符串?dāng)?shù)組革屠,需要使用 char **
表示:
如上圖可以看到,因?yàn)榇蛴?path[306]
出了異常排宰,所以數(shù)組總個(gè)數(shù)是 306似芝。另外,可以直接打印 x0 寄存器就可以知道 imageCount板甘,只不過(guò)上圖沒(méi)有空間顯示了党瓮。
上圖可以看到,逆序之后 libSystem
并不是處于第一個(gè)位置盐类,所以庫(kù)的優(yōu)先級(jí)和初始化順序大概有幾種:
- 優(yōu)先級(jí)大于 libSystem 的系統(tǒng)庫(kù)寞奸;
- libSystem;
- 系統(tǒng)庫(kù)在跳,如 libobjc 等枪萄;
- 工程中插入的動(dòng)態(tài)庫(kù);
- 主工程猫妙;
8. 總結(jié)
一張圖做個(gè)總結(jié)吧: