所用版本:
- 處理器: Intel Core i9
- MacOS 12.3.1
- Xcode 13.3.1
- objc4-838
熟悉類加載前, 先看下類的初始化方法_objc_init( 留意看下下面的注釋 ):
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
// 環(huán)境變量初始化
environ_init();
// 線程處理
tls_init();
// 運(yùn)行C++靜態(tài)構(gòu)造函數(shù)。
static_init();
// runtime運(yùn)行時(shí)初始化
runtime_init();
// objc異常處理系統(tǒng)初始化
exception_init();
#if __OBJC2__
// 緩存初始化
cache_t::init();
#endif
// 啟動(dòng)回調(diào)機(jī)制
_imp_implementationWithBlock_init();
// dyld通知注冊(cè)
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}

[ environ_init() ] 環(huán)境變量初始化

再次運(yùn)行可發(fā)現(xiàn), 新增打印信息

可看到打印了很多相關(guān)環(huán)境變量, OBJC_PRINT_IMAGES, OBJC_PRINT_CLASS_SETUP, OBJC_DISABLE_NONPOINTER_ISA等等。詳細(xì)見: Xcode環(huán)境變量說(shuō)明
[ tls_init ] 線程處理
針對(duì)本地線程處理做處理, 如果滿足SUPPORT_DIRECT_THREAD_KEYS析構(gòu), 不滿足初始化

其中
// Thread keys 由libc保留供我們使用。
# define SUPPORT_DIRECT_THREAD_KEYS 1 - 滿足 0 - 不滿足
- 判斷滿足:
pthread_key_init_np
pthread_key_init_np - 判斷不滿足:
tls_create
tls_create
重新初始化個(gè)線程key
[ static_init ] 運(yùn)行C++靜態(tài)構(gòu)造函數(shù)。
如果有C++靜態(tài)構(gòu)造函數(shù), libc會(huì)在dyld 調(diào)用_dyld_objc_notify_register之前, 先調(diào)用static_init執(zhí)行。

舉個(gè)例子, 我們寫一個(gè)全局構(gòu)造函數(shù), 運(yùn)行可發(fā)現(xiàn)


如圖可看出, 在_dyld_objc_notify_register之前如果有靜態(tài)C++構(gòu)造函數(shù), 那么通過(guò)static_init方法直接運(yùn)行。
[ runtime_init ] 運(yùn)行時(shí)初始化。

可看出主要分對(duì)兩部分, 分類初始化、類的表初始化進(jìn)行
[ exception_init ] objc異常處理系統(tǒng)初始化
初始化libobjc的異常處理系統(tǒng)。其實(shí)是注冊(cè)異常處理的回調(diào),從而監(jiān)控異常的處理

舉個(gè)例子:

數(shù)組越界例子肯定會(huì)發(fā)生crash, 接著我們運(yùn)行一下
先走了_objc_init中的exception_init

執(zhí)行old_terminate = std::set_terminate(&_objc_terminate);, 留意下此時(shí)還沒(méi)有執(zhí)行_dyld_objc_notify_register。

執(zhí)行_dyld_objc_notify_register → main

執(zhí)行_objc_terminate

最后crash

其實(shí)當(dāng) crash發(fā)生時(shí),會(huì)走_objc_terminate方法,接著走到uncaught_handler, 扔出異常并傳入一個(gè)參數(shù)(e), 而e的回調(diào)往下看

e = fn

可看出將objc_uncaught_exception_handler fn(設(shè)置的異常) 賦值給uncaught_handler, 即 uncaught_handler 等于 fn, 由此可看出uncaught_handler, 本質(zhì)是一個(gè)回調(diào)函數(shù)。

如圖,系統(tǒng)其實(shí)會(huì)針對(duì)crash進(jìn)行攔截處理,app會(huì)拋出一個(gè)異常句柄NSSetUncaughtExceptionHandler,傳入一個(gè)函數(shù)給系統(tǒng),當(dāng)異常發(fā)生后,調(diào)用函數(shù)(函數(shù)中可以線程?;?、收集并上傳崩潰日志),然后回到原有的app層中,其本質(zhì)是一個(gè)回調(diào)函數(shù)。
[cache_t::init()] 緩存初始化

[ _imp_implementationWithBlock_init ] 啟動(dòng)回調(diào)機(jī)制

[ _dyld_objc_notify_register ] dyld通知注冊(cè)
首先可以看到_dyld_objc_notify_register(&map_images, load_images, unmap_image);
3個(gè)參數(shù)&map_images、load_images、unmap_image
-
&map_images: 映射整個(gè)鏡像文件, 管理文件中, 動(dòng)態(tài)庫(kù)所有符號(hào) (class, Protocol, selector, category)
先留意下&, 說(shuō)明是指針傳遞, 傳遞是一個(gè)函數(shù)。這里用指針傳遞的好處是為了讓map_images同步發(fā)生變化, 主要原因這個(gè)函數(shù)很重要, 蘋果不希望它會(huì)因?yàn)橐恍┲貜?fù)加載發(fā)生錯(cuò)亂。同時(shí)這個(gè)映射操作也比較耗時(shí), 如果不是一起的話, 也會(huì)增加耗時(shí)性。看下其內(nèi)部

接下來(lái)看下map_images_nolock內(nèi)部

代碼有點(diǎn)長(zhǎng), 直接看重點(diǎn)代碼: 讀取鏡像_read_images

read_images這個(gè)方法很重要, 先說(shuō)下_read_images都做了什么
_read_images
- 條件控制進(jìn)行一次加載
- 修復(fù)預(yù)編譯階段的
@selector混亂問(wèn)題 - 錯(cuò)誤混亂的類處理
- 修復(fù)重映射一些沒(méi)有被鏡像文件加載進(jìn)來(lái)的類
- 修復(fù)消息
- 如果類里面有協(xié)議讀取
- 分類處理
- 類的加載處理
- 優(yōu)化類

接下來(lái)看下_read_images底層實(shí)現(xiàn), 并依次看下上面內(nèi)容

① 第一次加載

略過(guò)一些代碼看重點(diǎn)NXCreateMapTable

可看出第一次加載會(huì)創(chuàng)建一個(gè)表(key-value 哈希表): gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
- NXStrValueMapPrototype: 開辟類型
- namedClassesSize: 開辟總?cè)莘e
創(chuàng)建一張類的總表,這個(gè)表包含所有的類。4/3 因子這個(gè)我稍微講一下 , 先了解哈希表負(fù)載因子一個(gè)概念
哈希表負(fù)載因子
負(fù)載因子=總鍵值對(duì)數(shù)/數(shù)組的個(gè)數(shù)。負(fù)載因子是哈希表的一個(gè)重要屬性,用來(lái)衡量哈希表的空/滿程度,一定程度也可以提現(xiàn)查詢的效率。負(fù)載因子越大,意味著哈希表越滿,越容易導(dǎo)致沖突,性能也就越低。所以當(dāng)負(fù)載因子大于某個(gè)常數(shù)(一般是0.75 即 3 / 4)時(shí),哈希表將自動(dòng)擴(kuò)容。哈希表擴(kuò)容時(shí),一般會(huì)創(chuàng)建兩倍于原來(lái)的數(shù)組長(zhǎng)度,因此即使key的哈希值沒(méi)有變化,對(duì)數(shù)組個(gè)數(shù)取余的結(jié)果會(huì)隨著數(shù)組個(gè)數(shù)的擴(kuò)容發(fā)生變化,因此鍵值對(duì)的位置都有可能發(fā)生變化,這個(gè)過(guò)程也成為重哈希(rehash)。
那么回來(lái)再看下, 表的大小也遵循負(fù)載因子,這里 namedClassesSize = totalClasses * 4 / 3相當(dāng)于是負(fù)載因子``3/4的逆過(guò)程。namedClassesSize相當(dāng)于總?cè)萘?,totalClasses相當(dāng)于要占用的空間。
例如我們想創(chuàng)建一張表 , 總?cè)莘e: totalClass = x * 4 / 3
開辟表大小 x = totalClass * (3 / 4) = x * (4 / 3) * (3 / 4) = x = namedClassesSize
- 先看下
gdb_objc_realized_classes:
gdb_objc_realized_classes
其實(shí)gdb_objc_realized_classes是一張總表含所以類的表, 而runtime_init中的allocatedClasses
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}

可看出
allocatedClasses只是一個(gè)alloc的分表. gdb_objc_realized_classes包含它。
② 修復(fù)預(yù)編譯階段的@selector

- sel是在dyld和llvm的時(shí)候加載的。
- sels[i]是從mach-o獲取的 mach-o會(huì)有相對(duì)內(nèi)存地址和偏移地址。
sel 會(huì)有 名字 + 地址, 有些時(shí)候名字可能相同但是地址不相同, 這個(gè)時(shí)候需要修復(fù)一下

其中_getObjc2SelectorRefs是獲取Mach-O中的靜態(tài)段__objc_selrefs
GETSECT(_getObjc2SelectorRefs, SEL, "__objc_selrefs");
再看下sel_registerNameNoLock方法


調(diào)成一致, 將SEL覆蓋到中namedSelectors集合Set對(duì)應(yīng)位置上, 這里用Set原因: 雖然都是集合但是相比array, set處理hash方面效率確實(shí)是更高一些
舉個(gè)例子: 比如你要存儲(chǔ)元素A, 一個(gè)hash算法直接就能直接找到A應(yīng)該存儲(chǔ)的位置; 同樣, 當(dāng)你要訪問(wèn)A時(shí), 一個(gè)hash過(guò)程就能找到A存儲(chǔ)的位置. 而對(duì)于array,若想知道A到底在不在數(shù)組中, 則需要便利整個(gè)數(shù)組, 顯然效率較低了;
綜上: UnfixedSelectors修復(fù)sel就是把相不同的@selector統(tǒng)一化, 同時(shí)要以dyld的sel為準(zhǔn).
③ 錯(cuò)誤/混亂類處理

主要是從Mach-O中取出所有類,在遍歷進(jìn)行讀取, 核心方法readClass
我們看下它的底層
[readClass]

先加一個(gè)打印, 看看都能讀到什么類
printf("%s - Test - %s \n", __func__, mangledName);


可看出能把系統(tǒng)類和自定義類都讀取到, 沒(méi)有用到的自定義類也會(huì)讀取, 自定義類后添加的先讀取。接下來(lái)我們跟一下自定義類的流程
const char *SRTest = "SRTest";
// 是否匹配
if (strcmp(mangledName, SRTest) == 0) {
printf("%s - 當(dāng)前類 - %s \n", __func__, mangledName);
}

運(yùn)行發(fā)現(xiàn)SRTest已進(jìn)入

先走修正方法

如果類要求穩(wěn)定, 那么會(huì)修正下不穩(wěn)定的類

接下來(lái)跟流程可發(fā)現(xiàn)會(huì)走addNamedClass

[addNamedClass]
稍微看下addNamedClass內(nèi)部實(shí)現(xiàn)


addNamedClass將當(dāng)前類添加到之前創(chuàng)建好的gdb_objc_realized_classes總表中
(之前有寫, 往上翻第一次加載會(huì)創(chuàng)建一個(gè)表(key-value 哈希表): gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);)
繼續(xù)跟流程可發(fā)現(xiàn)走addClassTableEntry
[addClassTableEntry]


將類和元類插入allocatedClasses表中。這張表是在runtime_init中創(chuàng)建的。(之前也有寫, 往上翻runtime_init )
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
之后就會(huì)走readClass中的return cls;方法返回
綜上,可看出readClass的主要將Mach-O中的類, 添加進(jìn)內(nèi)存 (插入到表中, 總表, alloc的分表都插一份)



