目錄
前言
_objc_init方法
environ_init
tis->init方法
static_init方法
💡 _objc_init 是由 libc 調用的,目的是:
??“必須自己實現” 是什么意思?
runtime_init
exception_init
cache_t::init
_imp_implementationWithBlock_init
_dyld_objc_notify_register
dyld與Objc的關聯
map_images的調用時機
dyld與Objc關聯
map_images
_read_images
創建表
修復預編譯階段的@selector的混亂問題
錯誤混亂的類處理
修復重映射一些沒有被鏡像文件加載進來的類
修復一些消息
當類里面有協議時:readProtocol 讀取協議
修復沒被加載的協議
分類處理
類的加載處理
沒有被處理的類,優化那些被侵犯的類
關鍵步驟
readClass
realizeClassWithoutSwift
讀取 data 數據,并設置 ro、rw
遞歸調用 realizeClassWithoutSwift 完善 繼承鏈
通過 methodizeClass 方法化類
methodizeClass
attachToClass方法
attachCategories方法
attachLists方法
分類
分類的本質
分類的加載
分類加載時機
load_images
prepare_load_methods實現
schedule_class_load方法
add_category_to_loadable_list
call_load_methods
initalize分析
前言
其實在之前分析dyld的加載流程的時候已經有涉及到一些有關類和分類加載的過程了,這篇文章來探索一下類和分類的加載過程在底層的實現,研究的起點是_objc_init。
_objc_init方法
可以看出來_objc_init的實現中主要是調用了許多的init方法。
environ_init
environ_init()方法是初始化一系列環境變量,并讀取影響運行時的環境變量。
常用的環境變量有以下這些:
-
DYLD_PRINT_STATISTICS:設置 DYLD_PRINT_STATISTICS 為YES,控制臺就會打印 App 的加載時長,包括整體加載時長和動態庫加載時長,即main函數之前的啟動時間(查看pre-main耗時),可以通過設置了解其耗時部分,并對其進行啟動優化
-
OBJC_DISABLE_NONPOINTER_ISA:杜絕生成相應的nonpointer isa(nonpointer isa指針地址 末尾為1 ),生成的都是普通的isa
-
OBJC_PRINT_LOAD_METHODS:打印 Class 及 Category 的 + (void)load 方法的調用信息
-
NSDoubleLocalizedStrings:項目做國際化本地化(Localized)的時候是一個挺耗時的工作,想要檢測國際化翻譯好的語言文字UI會變成什么樣子,可以指定這個啟動項.可以設置 NSDoubleLocalizedStrings 為YES
-
NSShowNonLocalizedStrings:在完成國際化的時候,偶爾會有一些字符串沒有做本地化,這時就可以設置NSShowNonLocalizedStrings 為YES,所有沒有被本地化的字符串全都會變成大寫
tis->init方法
tls->init()方法是關于線程key的綁定,主要是本地線程池的初始化以及析構。
static_init方法
static_init()方法注釋中提到該方法會運行C++靜態構造函數(只會運行系統級別的構造函數) 在dyld調用靜態構造函數之前,libc會調用_objc_init,所以必須自己去實現。
為什么要在static_init之前調用_objc_init:
💡
_objc_init
是由 libc 調用的,目的是:在所有 ObjC 相關代碼執行之前,先初始化 ObjC Runtime(注冊類、創建基本元類、初始化 TLS、Hook、AutoreleasePool 等)。
這意味著:
dyld
還沒執行 C++ 靜態構造函數而我們已經通過
_objc_init
把 ObjC 的 runtime 環境搭建好了這樣之后無論運行什么構造函數,如果里面使用了 ObjC 的對象/類/方法,都不會崩潰
??“必須自己實現” 是什么意思?
這是指 Objective-C Runtime 不能依賴 dyld 的
static_init()
去自動初始化自己(因為它太晚了),所以:ObjC Runtime 必須自己注冊一個早期初始化入口 ——
_objc_init
,并讓 libc 來調用它。這個初始化包括:
注冊 TLS
初始化 runtime 狀態(
runtime_init()
)hook 一些系統函數(如
malloc
,pthread
等)初始化 autorelease pool
加載 image 等
runtime_init
運行時初始化,主要分為兩部分:分類初始化、類的表初始化。
exception_init
exception_init()負責初始化libobjc的異常處理系統,注冊異常處理的回調,從而監控異常的處理
cache_t::init
負責緩存初始化
_imp_implementationWithBlock_init
啟動回調機制
_dyld_objc_notify_register
這個方法就是注冊dyld,具體實現之前已經在分析dyld加載流程的時候分析過了。
從_dyld_objc_notify_register方法的注釋中可以得出:
-
僅供objc運行時使用
-
注冊處理程序,以便在映射、取消映射和初始化objc圖像時調用
-
dyld將會通過一個包含objc-image-info的鏡像文件的數組回調mapped函數
_dyld_objc_notify_register中的三個參數含義如下:
-
map_images:dyld將image(鏡像文件)加載進內存時,會觸發該函數
-
load_image:dyld初始化image會觸發該函數
-
unmap_image:dyld將image移除時,會觸發該函數
dyld與Objc的關聯
dyld源碼中_dyld_objc_notify_register的實現
libobjc源碼中_dyld_objc_notify_register的調用
結合這兩段代碼可以得出:
-
mapped
等價于map_images
-
init
等價于load_images
-
unmapped
等價于unmap_image
再在dyld源碼中查看registerObjCNotifiers的實現:
可以看到作為參數傳進去的三個函數一一被用來進行了賦值操作,所以會存在以下等價關系:
-
sNotifyObjCMapped == mapped == map_images
-
sNotifyObjCInit == init == load_images
-
sNotifyObjCUnmapped == unmapped == unmap_image
map_images的調用時機
關于load_images的調用時機在講述dyld的加載流程時已經講解過了,在notifySingle方法中,通過sNotifyObjCInit來調用。接下來我們分析一下map_images的調用時機
在dyld中全局搜索sNotifyObjCMapped,可以發現是在notifyBatchPartial方法中調用的
搜索notifyBatchPartial,可以看到它是在registerObjCNotifiers方法中調用的
到這里,我們再來梳理一遍dyld的流程:
-
在recursiveInitialization方法中會調用bool hasInitializers = this->doInitialization(context);這個方法是用來判斷image是否已加載。
-
同時doInitialization這個方法會調用doImageInit和doModInitFunctions(context),這兩個方法會進入libSystem框架里調用libSystem_initializer方法,最后就會調用_objc_init方法
-
_objc_init會調用_dyld_objc_notify_register將map_images、load_images、unmap_image傳入dyld方法registerObjCNotifiers
-
在registerObjCNotifiers方法中,我們把_dyld_objc_notify_register傳入的map_images賦值給sNotifyObjCMapped,將load_images賦值給sNotifyObjCInit,將unmap_image賦值給sNotifyObjCUnmapped
-
在registerObjCNotifiers方法中,我們將傳參賦值后就開始調用notifyBatchPartial()
-
notifyBatchPartial方法中會調用(*sNotifyObjCMapped)(objcImageCount, paths, mhs);觸發map_images方法
-
dyld的recursiveInitialization方法在調用完bool hasInitializers = this->doInitialization(context)方法后,會調用notifySingle()方法
-
在notifySingle()中會調用(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());上面我們將load_images賦值給了sNotifyObjCInit,所以此時就會觸發load_images方法
-
sNotifyObjCUnmapped會在removeImage方法里觸發,字面理解就是刪除Image(映射的鏡像文件)
所以有以下結論:map_images是先于load_images調用,即先map_images,再load_images。
dyld與Objc關聯
結合以上分析和之前對dyld的分析,可以總結出dyld與Objc的關聯就是通過回調完成的:
-
在
dyld中
注冊回調函數,可以理解為添加觀察者
-
在
objc
中dyld
注冊,可以理解為發送通知
-
觸發回調
,可以理解為執行通知selector
下面我們來看看map_images、load_images、unmap_image都做了什么
-
map_images
:主要是管理文件中和動態庫中
所有的符號
,即class、protocol、selector、category
等 -
load_images
:加載執行load方法
-
unmap_image
: 卸載移除數據
map_images
先說明為什么map_images有&,而load_images沒有:
-
map_images是 引用類型,外界變了,跟著變
-
load_images是值類型,不傳遞值
map_images的作用是將Mach-O中的類信息加載到內存,map_images調用map_images_nolock,map_images_nolock調用_read_images來加載鏡像文件
_read_images
_read_images這個函數的作用主要是加載類信息,即類、分類、協議等
先大致說一下_read_images的流程:
-
條件控制進行的一次加載——創建表(類的哈希表)
-
修復預編譯階段的@selector的混亂問題
-
錯誤混亂的類處理
-
修復重映射一些沒有被鏡像文件加載進來的類
-
修復一些消息
-
當類里面有協議時:readProtocol讀取協議
-
修復沒有被加載的協議
-
分類處理
-
類的加載處理
-
沒有被處理的類,優化那些被侵犯的類
現在看不懂沒關系,接下來我們來一個部分一個部分講清楚這個函數做了些什么
創建表
這個部分只有在第一次進入函數時會執行,他會創建一個用來存放類的哈希表,這個表里存放的類是那些不在共享緩存且已命名的類,無論類是否實現,容量是類數量的4/3。
修復預編譯階段的@selector的混亂問題
這段代碼的作用簡單地說可以用一句話概括:把編譯時生成的“假的 selector 地址”,統一替換成運行時真正注冊過的“合法 selector 地址”。
為什么會混亂?
@selector(foo) 編譯器生成的是靜態字符串指針(在 __objc_selrefs 中)。
但在 runtime 調用方法時,必須保證 SEL 是統一注冊過、地址唯一的。
如果不統一注冊,那么同樣的 @selector(foo) 在多個模塊中可能是不同地址,會導致消息發送、isEqual, NSStringFromSelector 等出現 bug。
打一個通俗的比方:
你可以把 Selector (@selector(...)
) 想象成你給朋友寫的一封信的收件人地址。
-
編譯器階段寫的是:“張三,某市某區某號”,但這是你隨手寫的,可能不是張三真正的地址。
-
運行時系統需要確認:“張三”真實住在哪里?(selector 實際對應哪塊內存)
-
如果你不校正這個地址,信可能寄不到正確的人手中,或者你認為兩個“張三”其實是不同的人。
所以這個方法做的事就是:
-
翻出所有你寫的“張三地址”(selector 指針)
-
拿去“戶籍局”查一查(通過
sel_registerNameNoLock
注冊) -
如果地址不對,系統會換成正確的地址
錯誤混亂的類處理
這里其實就是把Mach-O文件中的所有類都取出來,再遍歷進行處理。
在readClass之前,cls只是一個地址,在執行完readClass之后,原始的類地址才能被解析為一個有效的類對象。
除此之外,這里還會檢查共享緩存中的類是否已被覆蓋,如果覆蓋就需要重新處理。
對于未來類,如果解析后的類地址發生變化(即if (newCls != cls && newCls)
),就記錄到resolvedFutureClasses數組,這些類需要后續非懶加載初始化(立即分配可讀寫數據class_rw_t
)。
這段代碼的作用通俗來說,可以想象成你在整理一個雜亂的工具箱,確保所有工具都放在正確的位置,并且能正常使用。具體來說:
檢查是否有“外人改動” 先看一眼工具箱,確認有沒有人偷偷替換了里面的工具(比如通過插件或特殊配置修改了系統默認的類)。
逐個翻找工具箱的每個分區 打開每一個小格子(每個程序依賴的庫或框架),看看里面裝的是什么。
判斷是否需要整理 如果某個分區已經是整理好的(預優化過的類),就跳過不管;如果被改動過,才需要手動處理。
拿出所有工具的“設計圖紙” 從分區的某個固定位置(Mach-O文件的
__objc_classlist
段)掏出一疊設計圖,這些圖紙對應著程序里所有定義好的類。核對每張設計圖 把圖紙一張張展開檢查:
如果是正常的圖紙(普通類),直接貼上標簽(類名),放到對應位置。
如果發現某張圖紙寫的是“臨時占位符”(未來類),比如之前不知道這個類具體長什么樣,現在終于找到真正的圖紙了,就立刻替換掉占位符,并記錄下來:“這幾個類需要馬上組裝好,不能偷懶”。
標記需要立刻組裝的工具 把那些替換過占位符的類單獨記在小本本上(
resolvedFutureClasses
數組),后續要立刻把它們拼裝成完整的工具(分配內存、關聯方法等),而不是等到第一次用的時候才臨時拼裝(懶加載)。
修復重映射一些沒有被鏡像文件加載進來的類
主要是將未映射的Class
和Super Class
進行重映射,也就是將編譯時的引用地址依賴的未確定的鏡像基地址修正為運行時實際的地址,確保所有類引用指向正確內存位置
修復一些消息
這個部分是在處理一些歷史遺留的特殊消息發送機制,確保它們能在新系統中正常工作
當類里面有協議時:readProtocol 讀取協議
從源碼和注釋中我們可以看出來,大致分為三步:
第一步,通過NXMapTable *protocol_map = protocols();創建protocol哈希表,表的名稱為protocol_map
第二步,通過_getObjc2ProtocolList 獲取到Mach-O中的靜態段__objc_protolist協議列表,即從編譯器中讀取并初始化protocol
循環遍歷協議列表,通過readProtocol方法將協議添加到protocol_map哈希表中
修復沒被加載的協議
在 Objective-C 的運行時中,協議 @protocol 也會在編譯階段生成引用,在 Mach-O 文件中它們會被放到一個叫 _objc_protorefs (與_objc_protolist不同)的段里。
在運行時加載鏡像(Mach-O 文件)時:某些協議可能在預編譯優化(preoptimized)階段就已經被指向了共享緩存(dyld shared cache)中的協議定義;但是如果鏡像是后來才加載的,比如動態庫或插件(Bundle),這些協議引用可能仍然指向一個未修正的地址(stub 或未來協議)。為了保證協議指針指向真正的定義,就要在這里修正它們。
這段話看起來復雜,可以理解成一句話:「在協議的真實定義還沒有被加載之前,其他地方就已經引用了它,所以在協議真正加載之后,必須把原來的引用修正為真實地址。」
分類處理
這段代碼主要是處理分類,需要在分類初始化并將數據加載到類后才執行,對于運行時出現的分類,將分類的發現推遲到對_dyld_objc_notify_register的調用完成后的第一個load_images調用為止。
這是為了解決啟動時加載過早的問題:
-
load_images()
是 Runtime 加載新 Mach-O 鏡像(如動態庫、插件)時的回調; -
didInitialAttachCategories
是一個標記,代表初次附加分類是否完成; -
Runtime 只有在收到
_dyld_objc_notify_register()
的通知后,才開始做真正的分類附加; -
因為某些分類定義可能在系統框架里過早加載,如果這時就處理分類可能錯過關聯主類(主類還沒加載)。
所以:
👉 為了避免“分類加載時主類還沒準備好”的問題,分類的附加操作被延后到:
-
_dyld_objc_notify_register()
調用完; -
并且是在 第一個 load_images() 時執行;
-
保證主類和分類都已在內存中,分類才能正確附加上。
總結成一句話就是:Runtime 為了保證分類附加的時機正確,會延遲處理一些分類,直到確保主類已經加載,分類數據也加載完成之后,再統一合并附加。
那么問題來了:既然這段代碼是在map_images()這個函數里的,那怎么會在load_images()之后執行呢?
-
答案是當調用load_images()時,系統底層會調用 load_images() → 然后再次調用 map_images(),加載這個新鏡像。
那是不是意味著第一次調用map_images時,對于分類沒有進行任何操作呢?
-
答案是確實如此:分類是 編譯時寫入 Mach-O 文件的靜態結構,但只有在運行時才會“附加到主類上”,這個過程我們稱為 分類的附加(attach)。
-
map_images() 第一次執行時,不處理分類;只有在 Runtime 初始化完成后,再次調用 map_images() 時,才會在內部調用 load_categories_nolock() 去處理分類。
類的加載處理
這一段就是實現類的加載處理,實現非懶加載類
-
通過_getObjc2NonlazyClassList獲取Mach-O的靜態段__objc_nlclslist非懶加載類表(這是編譯器標記的那些類,它們要在程序啟動時就立刻初始化)
-
通過addClassTableEntry將非懶加載類插入類表,存儲到內存,如果已經添加就不會載添加,需要確保整個結構都被添加
-
通過realizeClassWithoutSwift實現當前的類,因為前面 ③中的readClass讀取到內存的僅僅只有地址+名稱,類的data數據并沒有加載出來
-
?? 真正實現這個類,realizeClassWithoutSwift這一步很關鍵:
-
把類的元信息(ro -> rw 等)建立起來;
-
設置方法列表、屬性、協議;
-
準備好實例大小、布局;
-
做好準備以便可以創建對象;
-
?? 如果類實現了
+load
,此時load
方法也會被調用。(錯誤,load在load_images階段調用)
-
關于懶加載類和非懶加載類:
如果實現了+load方法,就是非懶加載類,否則就是懶加載類
為什么實現load方法就會變成非懶加載類?:
因為
+load
是在類加載后立即執行的,如果類沒有先實現(realize),就無法安全執行+load
,也就可能錯過一些初始化邏輯,比如方法交換(swizzling)等。(load
方法會在load_images
調用)懶加載類在什么時候調用?:
只有在第一次使用(例如
alloc/init
)時才會加載。節省啟動性能。
沒有被處理的類,優化那些被侵犯的類
這一步負責處理在運行時動態解析的“未來類”(Future Classes),并確保它們被正確初始化
初始化未來類,如果是調試模式,強制初始化所有懶加載類(即使未被使用)
這里有一個誤區:懶加載類VS未來類
未來類與懶加載類是兩個完全獨立的概念,并且未來類不可能是懶加載類,懶加載類不可能是未來類。
關鍵步驟
在上述流程中,有兩個函數非常重要,分別是 readClass和realizeClassWithoutSwift
readClass
首先是readClass,readClass主要是讀取類,在未調用該方法前,cls只是一個地址,執行該方法后,cls是類的名稱
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{const char *mangledName = cls->nonlazyMangledName();//獲取類的名字
// printf("%s -哎呦不錯!- %s \n",__func__,mangledName);// tcj 玩的 ----如果想進入自定義的類,自己加一個判斷
// const char *TCJPersonName = "TCJPerson";
// if (strcmp(mangledName, TCJPersonName) == 0) {
// auto cj_ro = (const class_ro_t *)cls->data();
// printf("%s -- 哎呦不錯!--%s\n", __func__,mangledName);
// }//當前類的父類中若有丟失的weak-linked類,則返回nilif (missingWeakSuperclass(cls)) {// No superclass (probably weak-linked). // Disavow any knowledge of this subclass.if (PrintConnecting) {_objc_inform("CLASS: IGNORING class '%s' with ""missing weak-linked superclass", cls->nameForLogging());}addRemappedClass(cls, nil);cls->setSuperclass(nil);return nil;}cls->fixupBackwardDeployingStableSwift();//判斷是不是后期要處理的類//正常情況下,不會走到popFutureNamedClass,因為這是專門針對未來待處理的類的操作//通過斷點調試,不會走到if流程里面,因此也不會對ro、rw進行操作Class replacing = nil;if (mangledName != nullptr) {if (Class newCls = popFutureNamedClass(mangledName)) {// This name was previously allocated as a future class.// Copy objc_class to future class's struct.// Preserve future's rw data block.if (newCls->isAnySwift()) {_objc_fatal("Can't complete future class request for '%s' ""because the real class is too big.",cls->nameForLogging());}//讀取mach-o的data,設置ro、rw//經過調試,并不會走到這里class_rw_t *rw = newCls->data();const class_ro_t *old_ro = rw->ro();memcpy(newCls, cls, sizeof(objc_class));// Manually set address-discriminated ptrauthed fields// so that newCls gets the correct signatures.newCls->setSuperclass(cls->getSuperclass());newCls->initIsa(cls->getIsa());rw->set_ro((class_ro_t *)newCls->data());newCls->setData(rw);freeIfMutable((char *)old_ro->getName());free((void *)old_ro);addRemappedClass(cls, newCls);replacing = cls;cls = newCls;}}//判斷是否類是否已經加載到內存if (headerIsPreoptimized && !replacing) {// class list built in shared cache// fixme strict assert doesn't work because of duplicates// ASSERT(cls == getClass(name));ASSERT(mangledName == nullptr || getClassExceptSomeSwift(mangledName));} else {if (mangledName) { //some Swift generic classes can lazily generate their namesaddNamedClass(cls, mangledName, replacing);//加載共享緩存中的類} else {Class meta = cls->ISA();const class_ro_t *metaRO = meta->bits.safe_ro();ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");}addClassTableEntry(cls);//插入表,即相當于從mach-O文件 讀取到 內存 中}// for future reference: shared cache never contains MH_BUNDLEsif (headerIsBundle) {cls->data()->flags |= RO_FROM_BUNDLE;cls->ISA()->data()->flags |= RO_FROM_BUNDLE;}return cls;
}
readClass的流程主要分為以下幾步:
① 通過mangledName獲取類的名字
② 當前類的父類中若有丟失的weak-linked類(weak-linked 類(弱鏈接類)是一種特殊的類引用方式,允許應用在編譯時引用某個類,但在運行時才檢查該類是否實際存在),則返回nil
③ 通過addNamedClass將當前類添加到已經創建好的gdb_objc_realized_classes哈希表(名稱映射表),該表用于存放所有類
④ 通過addClassTableEntry,將已經可用的類結構(已讀入內存,已具備基本結構(isa、ro/rw 等))添加到allocatedClasses表(類哈希表),這個表在_objc_init中的runtime_init就初始化創建了
綜上所述,readClass的主要作用就是將Mach-O中的類讀取到內存,即插入表中(將 Mach-O 文件中解析出的 class 地址轉換為真正可用的類對象,并放入類表中,為后續使用做好準備),但是目前的類僅有兩個信息:地址以及名稱,而mach-O的其中的data數據還未讀取出來
realizeClassWithoutSwift
realizeClassWithoutSwift方法主要作用是實現類,將類的data數據加載到內存中,主要有以下幾部分操作:
-
① 讀取data數據,并設置ro、rw
-
② 遞歸調用realizeClassWithoutSwift完善繼承鏈
-
③ 通過methodizeClass方法化類
讀取 data 數據,并設置 ro、rw
這一步負責讀取class的data數據,并將其強轉為ro,以及rw初始化和ro拷貝一份到rw中的ro
-
ro 表示 readOnly,即只讀,其在編譯時就已經確定了內存,包含類名稱、方法、協議和實例變量的信息,由于是只讀的,所以屬于Clean Memory,而Clean Memory是指加載后不會發生更改的內存
-
rw 表示 readWrite,即可讀可寫,由于其動態性,可能會往類中添加屬性、方法、添加協議,但其實在rw中只有10%的類真正的更改了它們的方法,所以有了rwe,即類的額外信息。對于那些確實需要額外信息的類,分配一塊 rwe(這是一個rw的可選擴展字段,不需要額外信息的類就不會分配 rwe),并將其滑入類中供其使用。其中rw就屬于dirty memory,而 dirty memory是指在進程運行時會發生更改的內存,類結構一經使用就會變成 ditry memory,因為運行時會向它寫入新數據,例如創建一個新的方法緩存,并從類中指向它
遞歸調用 realizeClassWithoutSwift 完善 繼承鏈
遞歸調用realizeClassWithoutSwift完善繼承鏈,并設置當前類、父類、元類的rw
-
遞歸調用 realizeClassWithoutSwift設置父類、元類
-
設置父類和元類的isa指向
-
通過addSubclass 和 addRootClass設置父子的雙向鏈表指向關系,即父類中可以找到子類,子類中可以找到父類
在realizeClassWithoutSwift遞歸調用時,isa找到根元類之后,根元類的isa是指向自己,并不會返回nil,所以有以下遞歸終止條件,其目的是保證類只加載一次:
-
如果類不存在,則返回nil
-
如果類已經實現,則直接返回cls
通過 methodizeClass 方法化類
該方法會:
-
從
ro
中提取類本身的方法、屬性、協議,存入rw
的動態列表(如methods
、properties
、protocols
)。 -
從運行時獲取分類的方法、屬性、協議,合并到
rw
的對應列表中。 -
確保
rw
包含所有可訪問的成員,供運行時動態查詢。
🙋問題來了:之前已經用ro給rw賦過值了,為什么還要再給rw寫入方法、屬性和協議呢?
🔑原因是當我們對rw賦值后:
此時
rw
僅包含ro
的只讀數據副本,但未處理動態數據(如分類)。
rw
的方法列表、屬性列表等動態數據字段(如methods
、properties
、protocols
)尚未填充。
methodizeClass
關于methodizeClass,它的實現源碼如下:
static void methodizeClass(Class cls, Class previously)
{runtimeLock.assertLocked();
?bool isMeta = cls->isMetaClass();auto rw = cls->data();auto ro = rw->ro();auto rwe = rw->ext();
?// Methodizing for the first timeif (PrintConnecting) {_objc_inform("CLASS: methodizing class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : "");}
?// Install methods and properties that the class implements itself.method_list_t *list = ro->baseMethods();if (list) {prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);if (rwe) rwe->methods.attachLists(&list, 1);}
?property_list_t *proplist = ro->baseProperties;if (rwe && proplist) {rwe->properties.attachLists(&proplist, 1);}
?protocol_list_t *protolist = ro->baseProtocols;if (rwe && protolist) {rwe->protocols.attachLists(&protolist, 1);}
?// Root classes get bonus method implementations if they don't have // them already. These apply before category replacements.if (cls->isRootMetaclass()) {// root metaclassaddMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);}
?// Attach categories.if (previously) {if (isMeta) {objc::unattachedCategories.attachToClass(cls, previously,ATTACH_METACLASS);} else {// When a class relocates, categories with class methods// may be registered on the class itself rather than on// the metaclass. Tell attachToClass to look for those.objc::unattachedCategories.attachToClass(cls, previously,ATTACH_CLASS_AND_METACLASS);}}objc::unattachedCategories.attachToClass(cls, cls,isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
?
#if DEBUG// Debug: sanity-check all SELs; log method list contentsfor (const auto& meth : rw->methods()) {if (PrintConnecting) {_objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', cls->nameForLogging(), sel_getName(meth.name()));}ASSERT(sel_registerName(sel_getName(meth.name())) == meth.name());}
#endif
}
就像之前所說,methodizeClass主要就是兩步:
-
將
屬性列表、方法列表、協議列表
等貼到rwe
中 -
附加
分類
中的方法
rwe的邏輯:
方法列表
加入rwe
的邏輯如下:
-
獲取
ro
的baseMethods
-
通過
prepareMethodLists
方法排序 -
對
rwe
進行處理即通過attachLists
插入
prepareMethodLists
內部通過fixupMethodList
方法排序,排序的邏輯是根據selector address
排序。
attachToClass方法
在方法化類的方法methodizeClass中,還用到了幾個比較重要的方法,比如attachToClass方法,它進行的操作是將分類添加到主類中
attachToClass中的外部循環是找到一個分類就會進到attachCategories一次,即找一個就循環一次,在這個方法里可以確定分類和對應主類,在attachCategories方法中會進行數據、協議、方法的添加
attachCategories方法
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,int flags)
{if (slowpath(PrintReplacedMethods)) {printReplacements(cls, cats_list, cats_count);}if (slowpath(PrintConnecting)) {_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");}
?/** Only a few classes have more than 64 categories during launch.* This uses a little stack, and avoids malloc.** Categories must be added in the proper order, which is back* to front. To do that with the chunking, we iterate cats_list* from front to back, build up the local buffers backwards,* and call attachLists on the chunks. attachLists prepends the* lists, so the final result is in the expected order.*/constexpr uint32_t ATTACH_BUFSIZ = 64;method_list_t ? *mlists[ATTACH_BUFSIZ];property_list_t *proplists[ATTACH_BUFSIZ];protocol_list_t *protolists[ATTACH_BUFSIZ];
?uint32_t mcount = 0;uint32_t propcount = 0;uint32_t protocount = 0;bool fromBundle = NO;bool isMeta = (flags & ATTACH_METACLASS);auto rwe = cls->data()->extAllocIfNeeded();
?for (uint32_t i = 0; i < cats_count; i++) {auto& entry = cats_list[i];
?method_list_t *mlist = entry.cat->methodsForMeta(isMeta);if (mlist) {if (mcount == ATTACH_BUFSIZ) {prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);rwe->methods.attachLists(mlists, mcount);mcount = 0;}mlists[ATTACH_BUFSIZ - ++mcount] = mlist;fromBundle |= entry.hi->isBundle();}
?property_list_t *proplist =entry.cat->propertiesForMeta(isMeta, entry.hi);if (proplist) {if (propcount == ATTACH_BUFSIZ) {rwe->properties.attachLists(proplists, propcount);propcount = 0;}proplists[ATTACH_BUFSIZ - ++propcount] = proplist;}
?protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);if (protolist) {if (protocount == ATTACH_BUFSIZ) {rwe->protocols.attachLists(protolists, protocount);protocount = 0;}protolists[ATTACH_BUFSIZ - ++protocount] = protolist;}}
?if (mcount > 0) {prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,NO, fromBundle, __func__);rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);if (flags & ATTACH_EXISTING) {flushCaches(cls, __func__, [](Class c){// constant caches have been dealt with in prepareMethodLists// if the class still is constant here, it's fine to keepreturn !c->cache.isConstantOptimizedCache();});}}
?rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
?rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
attachCategories
函數負責將分類(Category)中的方法、屬性和協議合并到目標類中,確保分類內容在運行時生效。
auto rwe = cls->data()->extAllocIfNeeded();這一行是第一步,負責進行rwe的創建(因為現在要往本類
中添加屬性、方法、協議
等,即對原來的 clean memory
要進行處理了,所以需要先對rwe進行初始化)
進入extAllocIfNeeded
方法的源碼實現,判斷rwe
是否存在,如果存在則直接獲取,如果不存在則開辟
進入extAlloc
源碼實現,即對rwe 0-1
的過程,在此過程中,就將本類的data
數據加載進去了
需要注意的幾個點是:
-
逆序存儲正序附加:通過倒序填充緩沖區,正序附加,確保后加載的分類內容優先生效。
-
分批處理:每滿64個分類處理一次,平衡性能與內存占用。
-
方法覆蓋:分類方法插入到類方法列表頭部,實現“后編譯的分類覆蓋先編譯的類或分類方法”。
attachLists方法
注意到在添加分類的方法時,是通過attachLists方法插入數據的,并且不止方法,屬性和協議都是通過attachLists方法插入數據的。
其實,方法列表和屬性列表都繼承自entsize_list_tt,協議則是類似entsize_list_tt實現,都是二維數組。
那么我們來看看attachLists方法的實現:
從源碼中可以看見,插入表存在三種情況:
-
①
多對多
: 如果當前調用attachLists
的list_array_tt
二維數組中有多個一維數組
-
通過
malloc
根據新的容量大小,開辟一個數組,類型是 array_t,通過array()獲取 -
倒序遍歷把原來的數據移動到容器的末尾
-
遍歷新的數據移動到容器的起始位置
-
-
②
0對1
: 如果調用attachLists
的list_array_tt
二維數組為空且新增大小數目為 1
-
直接賦值
addedList
的第一個list
-
-
③
1對多
: 如果當前調用attachLists
的list_array_tt
二維數組只有一個一維數組
-
通過malloc開辟一個容量和大小的集合,類型是 array_t,即創建一個數組,放到array中,通過array()獲取
-
由于只有一個一維數組,所以直接賦值到新
Array
的最后一個位置 -
循環遍歷從數組起始位置存入新的list,其中array()->lists 表示首位元素位置
-
這就是為什么子類可以重寫父類的方法,也是為什么分類可以重寫類的方法,要加一個newlist
的目的是由于要使用這個newlist
中的方法,這個newlist
對于用戶的價值要高,即優先調用
總結一下:attachLists
方法主要是將類
和分類
的數據加載到rwe
中
-
首先
加載本類的data數據
,此時的rwe沒有數據為空
,走0對1
流程 -
當
加入一個分類
時,此時的rwe僅有一個list
,即本類的list
,走1對多
流程 -
再
加入一個分類
時,此時的rwe中有兩個list
,即本類+分類的list
,走多對多
流程
分類
從上面的內容中,我們已經知道怎么把分類加到主類上了,接下來我們從分類的角度來分析一下分類
分類的本質
首先探索一下分類的本質。給TCJPerson定義分類TCJ
用Clang進行反編譯得到C++代碼,可以看到分類是存儲在MachO
文件的__DATA
段的__objc_catlist
中,還可以看到TCJPerson
分類的結構。
可以發現TCJPerson改為_CATEGORY_TCJPerson_,并且被_category_t修飾,可以看到_category_t的結構
可以看見_category_t是個結構體,里面保存有名稱(類的名字)、cls、對象方法列表、類方法列表、協議、屬性
為什么分類的方法要將實例方法和類方法分開存呢?
分類有兩個方法列表是因為分類是沒有元分類的,分類的方法是在
運行時
通過attachToClass
插入到class
的
查看方法列表的反編譯代碼
可以看到有三個對象方法和一個類方法,格式為:sel+簽名+地址
,和method_t
結構體一樣
再看看屬性
發現存在屬性的變量名但是沒有對應的set和get方法
分類的加載
在之前的部分提到了類有懶加載類和非懶加載類,二者的加載時機不同,那么如果涉及到分類,又是何時進行加載呢?
首先先回顧一下分類是如何進行加載的:
-
分類數據在attachCategories方法中加載,分類的加載遵循這樣一個規則——越晚加進來,越在前面
-
在methodizeClass中,通過attatchToClass方法將分類數據添加到主類。methodizeClass方法中類的數據和分類數據分開處理,因為編譯階段已經確定好了
方法的歸屬位置
(即實例方法
存儲在類
中,類方法
存儲在元類
中),而分類
是后面才加進來的。
分類加載時機
關于分類的加載時機,有一條規律:只要有一個分類是非懶加載分類,那么所有的分類都會被標記位非懶加載分類。
因為加載一個分類,意味著類已經開辟了rwe,那么就不會再次懶加載,重新去處理主類了。
根據類和分類是否實現+load方法,我們可以得到4種情況:
-
非懶加載類+非懶加載分類:類在read_images中加載,而分類數據如之前所述在第一次進入read_images時,不會加載分類數據,所以此時無法合并分類。接著會再調用一次_load_images,隨后再次調用map_images,這時再運行到read_images時就可以成功methodizeClass,合并分類。
-
非懶加載類與懶加載分類:類會在read_images中加載,而分類不會像非懶加載分類一樣在read_images時合并到主類,而是會被暫時加入
_unattachedCategories
列表中,等待后續時機觸發合并 -
懶加載類與懶加載分類:第一次發送消息給類時,類會實現并且分類會合并到主類上
-
懶加載類與非懶加載分類:懶加載類 + 非懶加載分類的數據加載,只要分類實現了load,會迫使主類提前加載,即主類強行轉換為非懶加載類樣式
在這四種情況中,分類的數據都是在load_images調用map_images時read_class()來加載到內存的,區別只在于類何時實現以及分類何時附加到主類
load_images
load_images方法的主要作用是加載鏡像文件,其中有兩個比較重要的方法:prepare_load_methods
(加載) 和 call_load_methods
(調用)
Load_images源碼如下:
這里的加載所有分類,其實是在分類表中遍歷,檢查分類的主類是否已經實現,如果已經實現就把分類合并上去,否則放到 _unattachedCategories
中,這一步其實正是之前提到的對懶加載分類的處理。懶加載類實現了之后,在這里就會把分類合并上去
prepare_load_methods實現
如圖所示,這個方法會把類及其父類和分類的load方法都放到數組中。
這里有兩個方法:schedule_class_load、add_category_to_loadable_list
schedule_class_load方法
關于schedule_class_load方法:
這個方法根據類的繼承鏈遞歸調用獲取load,直到cls不存在才結束遞歸,這樣做的目的是為了確保父類的load優先加載
add_class_to_loadable_list:
在schedule_class_load方法中調用了這個方法,此方法主要是將load方法
和cls類名
一起加到loadable_classes
表中
getLoadMethod:
在add_class_to_loadable_list方法中調用了這個方法
這個方法主要就是用來獲取方法列表中sel為load的方法
add_category_to_loadable_list
主要是獲取所有的非懶加載分類中的load方法
,將分類名+load方法
加入表loadable_categories
call_load_methods
這個方法有三步操作:
-
反復調用
類的+load
,直到不再有 -
調用一次
分類的+load
-
如果有類或更多未嘗試的分類,則運行更多的
+load
方法的實現中主要就是兩個方法:call_class_loads和 call_category_loads
call_class_loads主要加載類的load方法,而call_category_loads主要是加載一次分類的load方法
initalize分析
關于initialize,它通常是在某個類接收到第一條消息之前調用,它的調用鏈是lookUpImpOrForward
->realizeAndInitializeIfNeeded_locked
->initializeAndLeaveLocked
->initializeAndMaybeRelock
->initializeNonMetaClass
在initializeNonMetaClass
遞歸調用父類initialize
,然后調用callInitialize
void initializeNonMetaClass(Class cls)
{ASSERT(!cls->isMetaClass());
?Class supercls;bool reallyInitialize = NO;
?// Make sure super is done initializing BEFORE beginning to initialize cls.// See note about deadlock above.supercls = cls->getSuperclass();if (supercls ?&& ?!supercls->isInitialized()) {initializeNonMetaClass(supercls);}// Try to atomically set CLS_INITIALIZING.SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;{monitor_locker_t lock(classInitLock);if (!cls->isInitialized() && !cls->isInitializing()) {cls->setInitializing();reallyInitialize = YES;
?// Grab a copy of the will-initialize funcs with the lock held.localWillInitializeFuncs.initFrom(willInitializeFuncs);}}if (reallyInitialize) {// We successfully set the CLS_INITIALIZING bit. Initialize the class.// Record that we're initializing this class so we can message it._setThisThreadIsInitializingClass(cls);
?if (MultithreadedForkChild) {// LOL JK we don't really call +initialize methods after fork().performForkChildInitialize(cls, supercls);return;}for (auto callback : localWillInitializeFuncs)callback.f(callback.context, cls);
?// Send the +initialize message.// Note that +initialize is sent to the superclass (again) if // this class doesn't implement +initialize. 2157218if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",objc_thread_self(), cls->nameForLogging());}
?// Exceptions: A +initialize call that throws an exception // is deemed to be a complete and successful +initialize.//// Only __OBJC2__ adds these handlers. !__OBJC2__ has a// bootstrapping problem of this versus CF's call to// objc_exception_set_functions().
#if __OBJC2__@try
#endif{callInitialize(cls);
?if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",objc_thread_self(), cls->nameForLogging());}}
#if __OBJC2__@catch (...) {if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: +[%s initialize] ""threw an exception",objc_thread_self(), cls->nameForLogging());}@throw;}@finally
#endif{// Done initializing.lockAndFinishInitializing(cls, supercls);}return;}else if (cls->isInitializing()) {// We couldn't set INITIALIZING because INITIALIZING was already set.// If this thread set it earlier, continue normally.// If some other thread set it, block until initialize is done.// It's ok if INITIALIZING changes to INITIALIZED while we're here, // ? because we safely check for INITIALIZED inside the lock // ? before blocking.if (_thisThreadIsInitializingClass(cls)) {return;} else if (!MultithreadedForkChild) {waitForInitializeToComplete(cls);return;} else {// We're on the child side of fork(), facing a class that// was initializing by some other thread when fork() was called._setThisThreadIsInitializingClass(cls);performForkChildInitialize(cls, supercls);}}else if (cls->isInitialized()) {// Set CLS_INITIALIZING failed because someone else already // ? initialized the class. Continue normally.// NOTE this check must come AFTER the ISINITIALIZING case.// Otherwise: Another thread is initializing this class. ISINITIALIZED // ? is false. Skip this clause. Then the other thread finishes // ? initialization and sets INITIALIZING=no and INITIALIZED=yes. // ? Skip the ISINITIALIZING clause. Die horribly.return;}else {// We shouldn't be here. _objc_fatal("thread-safe class init in objc runtime is buggy!");}
}
callInitialize是一個普通的消息發送
關于initialize:
-
initialize
在類或者其子類的第一個方法被調用前(發送消息前)調用 -
只在類中添加
initialize
但不使用的情況下,是不會調用initialize
-
父類的
initialize
方法會比子類先執行 -
當子類未實現
initialize
方法時,會調用父類initialize
方法;子類實現initialize
方法時,會覆蓋父類initialize
方法 -
當有多個分類都實現了
initialize
方法,會覆蓋類中的方法,只執行一個(會執行最后被加載到內存中的分類的方法)