OC底層探索(十三): 類的加載(一)

所用版本:

  • 處理器: 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
}
_objc_init

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

打印準(zhǔ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), 不滿足初始化

tls_init

其中

// 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í)行。

static_init

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

全局構(gòu)造函數(shù)
打印結(jié)果

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

[ runtime_init ] 運(yùn)行時(shí)初始化。

runtime_init

可看出主要分對(duì)兩部分, 分類初始化、類的表初始化進(jìn)行

[ exception_init ] objc異常處理系統(tǒng)初始化

初始化libobjc的異常處理系統(tǒng)。其實(shí)是注冊(cè)異常處理的回調(diào),從而監(jiān)控異常的處理

exception_init

舉個(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_registermain

例子

執(zhí)行_objc_terminate

例子

最后crash
例子

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

`uncaught_handler `

e = fn

uncaught_handler

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

應(yīng)用級(jí)crash

如圖,系統(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ī)制

_imp_implementationWithBlock_init

[ _dyld_objc_notify_register ] dyld通知注冊(cè)

首先可以看到_dyld_objc_notify_register(&map_images, load_images, unmap_image);
3個(gè)參數(shù)&map_images、load_imagesunmap_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)部

map_images

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

map_images_nolock

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

_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)化類
_read_images

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

_read_images

① 第一次加載
doneOnce

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

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

  1. 先看下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

可看出allocatedClasses只是一個(gè)alloc的分表. gdb_objc_realized_classes包含它。

② 修復(fù)預(yù)編譯階段的@selector
修復(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方法

sel_registerNameNoLock
__sel_registerName

調(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]
readClass

先加一個(gè)打印, 看看都能讀到什么類

 printf("%s - Test - %s \n", __func__, mangledName);
打印
打印結(jié)果

可看出能把系統(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)入

運(yùn)行

先走修正方法


fixupBackwardDeployingStableSwift

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


fixupBackwardDeployingStableSwift

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

走addNamedClass

[addNamedClass]

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

addNamedClass

addNamedClass

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]
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的分表都插一份)

readClass
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容