了解Runtime有助于我們理解
Objective-C運(yùn)行時系統(tǒng)的工作原理以及如何利用它。本章將介紹NSObject類以及Objective-C程序如何與運(yùn)行時系統(tǒng)進(jìn)行交互,如何在運(yùn)行時查找對象的信息,如何將消息轉(zhuǎn)發(fā)給其他對象。
Runtime簡介
Runtime 又稱運(yùn)行時,是iOS系統(tǒng)的核心,它是一套底層的C語言API。它會將一些工作放在代碼運(yùn)行時才處理而并非編譯時,所以有很多類和成員變量在我們編譯時是不知道的,而在運(yùn)行時,我們所編寫的代碼會轉(zhuǎn)換成完整的確定的代碼運(yùn)行。
運(yùn)行時交互
在Objective-C中與運(yùn)行時系統(tǒng)有三個層次的交互:
- 通過
Objective-C源碼 - 通過
Foundation中的NSObject定義的方法 - 通過直接調(diào)用運(yùn)行時函數(shù)
Objective-C 源代碼
顧名思義,只要編寫和編譯 Objective-C代碼即可使用它。編譯包含Objective-C 類和方法的代碼時,編譯器會創(chuàng)建實(shí)現(xiàn)語言動態(tài)特性的數(shù)據(jù)結(jié)構(gòu)和函數(shù)。
NSObject 方法
在Cocoa 中,大多數(shù)對象都是NSObject的子類,因此都繼承了他定義的方法。(NSProxy是個特例)
NSObject中有一些方法可以簡單地向運(yùn)行時系統(tǒng)查詢信息。支持對象執(zhí)行自省。例如class方法,有isKindOfClass: 和 isMemberOfClass:, 檢查對象在繼承層級結(jié)構(gòu)的位置是否正確;
conformsToProtocol:,對象是否要實(shí)現(xiàn)特定協(xié)議中定義的方法;respondsToSelector:,表示對象是否可以接受特定消息;
// 檢查對象在繼承層級結(jié)構(gòu)的位置;
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
// 指示對象是否聲稱要實(shí)現(xiàn)特定協(xié)議中定義的方法
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
// 表示對象是否可以接受特定消息 (檢查是否實(shí)現(xiàn)某個方法)
- (BOOL)respondsToSelector:(SEL)aSelector;
// 方法實(shí)現(xiàn)的地址
- (IMP)methodForSelector:(SEL)aSelector;
運(yùn)行時函數(shù)
運(yùn)行時系統(tǒng)是一個共享的動態(tài)庫,公共的接口在目錄下/usr/include/objc。其中包含了一些函數(shù)和數(shù)據(jù)結(jié)構(gòu),許多函數(shù)可以使用純C來復(fù)制編譯器在編寫 Objective-C 代碼時所做的事情。這其實(shí)也就是我們常常看到的,使用某些運(yùn)行時函數(shù)可以達(dá)到可以NSObject方法一樣的效果,其實(shí)也正是這些底層的函數(shù)構(gòu)成了NSObject的基礎(chǔ)功能。這里是官方文檔Objective-C 運(yùn)行時參考。
現(xiàn)在,我們知道了運(yùn)行時交互有哪些,那么接下來,我們再看看Objective-C中的一些基本概念:類、對象、Method、SEL、IMP。熟悉這些概念之后,會更加理解運(yùn)行時做了哪些事。
類、對象、Method、SEL、IMP
類
類對象(Class)是由程序設(shè)置后在運(yùn)行時由編譯器創(chuàng)建的,當(dāng)一個對象的實(shí)例方法被調(diào)用時,會通過isa找到這個類,然后在該類中方法列表中查找。
// Class 定義
typedef struct objc_class *Class;
// 類結(jié)構(gòu)體
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
結(jié)構(gòu)體里有指向父類的指針、類名、版本、實(shí)例大小、實(shí)例變量列表、方法列表、緩存、遵守的協(xié)議列表等。
那疑問來了,請問類方法是存在哪里的?我們在調(diào)用類方法的時候,我們?nèi)绾稳フ夷兀窟@里就引入一個概念:元類(meta-class)。(想要深入了解元類可以查看這篇文章 What is a meta-class in Objective-C?)
元類就是類對象的isa指向的類,也可以說是類對象的類
對象
實(shí)例對象(Object)是我們對類對象alloc或者new操作時所創(chuàng)建的,在這個過程中會拷貝實(shí)例所屬的類的成員變量,但并不拷貝類定義的方法。調(diào)用實(shí)例方法時,系統(tǒng)會根據(jù)實(shí)例的isa指針去類的方法列表及父類的方法列表中尋找與消息對應(yīng)的selector指向的方法
// 對象結(jié)構(gòu)體
struct objc_object {
Class _Nonnull isa; OBJC_ISA_AVAILABILITY;
};
由此,我們得出了一個結(jié)論,類對象和實(shí)例對象的查找機(jī)制是一樣的:
- 對象的實(shí)例方法調(diào)用時,通過對象的 isa 在類中獲取方法的實(shí)現(xiàn)。
- 類對象的類方法調(diào)用時,通過類的 isa 在元類中獲取方法的實(shí)現(xiàn)。
對應(yīng)關(guān)系如下圖,描述了對象,類,元類之間的關(guān)系:

圖中實(shí)線是 super_class指針,虛線是isa指針。
- Root class (class)就是NSObject,NSObject沒有超類,所以Root class(class)的superclass指向nil。
- 每個Class都有一個isa指向唯一的Meta class
- Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個回路。
- 每個Meta class的isa都指向Root class (meta)。
Method
Method就是我們平時所說的函數(shù),它表示的是能夠獨(dú)立完成一個功能的一段代碼;Method通過selector和IMP兩個屬性,實(shí)現(xiàn)了快速查詢方法及實(shí)現(xiàn),相對提高了性能,又保持了靈活性。
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp
SEL
SEL是方法選擇器, 常見的寫法有:@selector()
/// 代表一個方法的不透明類型
typedef struct objc_selector *SEL;
聲明方式:
SEL s1 = @selector(test1);
SEL s2 = NSSelectorFromString(@"test2");
IMP
IMP是指向最終實(shí)現(xiàn)程序的內(nèi)存地址的指針,下面是它的定義:
/// 指向一個方法實(shí)現(xiàn)的指針
typedef id (*IMP)(id, SEL, ...);
#endif
理解了前面的這些概念之后,接下來我們進(jìn)入正題。
在 Objective-C 中,消息直到運(yùn)行時才綁定到方法實(shí)現(xiàn)。編譯器轉(zhuǎn)換消息表達(dá)式,調(diào)用objc_msgSend方法。
消息發(fā)送
Objective-C 中所有方法的調(diào)用/類的生成都在運(yùn)行時進(jìn)行,我們可以通過類名/方法名反射得到相應(yīng)的類和方法,也可以替換某個類的方法為新的實(shí)現(xiàn),理論上你可以在運(yùn)行時通過類名/方法名調(diào)用到任何 Objective-C 方法,替換任何類的實(shí)現(xiàn)以及新增任意類。
比方說我們寫一個調(diào)用方法[receiver message],那這個方法會被編譯器轉(zhuǎn)化成:
// 第一個參數(shù)類型是發(fā)送者, 第二個參數(shù)類型是SEL。SEL在OC中是selector方法選擇器
id objc_msgSend ( id _Nullable self, SEL op, ... );
不同類中相同名字的方法所對應(yīng)的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導(dǎo)致它們具有相同的方法選擇器。由于這點(diǎn)特性,也導(dǎo)致了Objective-C不支持函數(shù)重載。
實(shí)際上,我們在調(diào)用的方法的過程,其實(shí)在Runtime中就是消息發(fā)送。
objc_msgSend的實(shí)現(xiàn)是由匯編語言實(shí)現(xiàn),根據(jù)CPU架構(gòu)實(shí)現(xiàn)的過程各不相同,如果想閱讀相關(guān)的代碼要有一定的匯編基礎(chǔ);
objc_msgSend會做以下幾件事情:
1.檢測這個 selector是不是要忽略
-
2.檢查target是不是為nil
- 如果這里有相應(yīng)的nil的處理函數(shù),就跳轉(zhuǎn)到相應(yīng)的函數(shù)中
- 如果沒有處理nil的函數(shù),就自動清理并返回。這一點(diǎn)就是為何在
Objective-C中給nil發(fā)送消息不會崩潰的原因
-
3.確定不是給nil發(fā)消息之后,在該class的緩存中查找方法對應(yīng)的IMP實(shí)現(xiàn)
- 如果找到,就跳轉(zhuǎn)進(jìn)去執(zhí)行
- 如果沒有找到,就在方法列表里面繼續(xù)查找,一直找到NSObject為止
4.如果還沒有找到,那就需要開始消息轉(zhuǎn)發(fā)階段了。至此,發(fā)送消息Messaging階段完成。這一階段主要完成的是通過select()快速查找IMP的過程
消息傳遞框架:

為了加快消息傳遞過程,運(yùn)行時系統(tǒng)會在使用方法時緩存方法的選擇器和地址。每個類都有一個單獨(dú)的緩存,它可以包含繼承方法以及類中定義的方法的選擇器。在消息傳遞過程中,會首先檢查接收對象類的緩存。
消息轉(zhuǎn)發(fā)
轉(zhuǎn)發(fā)階段,會調(diào)用_objc_msgForward(id self, SEL _cmd,...)方法
_objc_msgForward(id _Nonnull receiver, SEL _Nonnull sel, ...)
_objc_msgForward會做以下幾件事情:
- 1.先調(diào)用
forwardingTargetForSelector方法獲取新的 target 作為 receiver 重新執(zhí)行 selector,- 如果返回的內(nèi)容合法, 跳轉(zhuǎn)去執(zhí)行
- 如果返回的內(nèi)容不合法(為 nil 或者跟舊 receiver 一樣),繼續(xù)執(zhí)行后續(xù)方法。
- 2.調(diào)用
methodSignatureForSelector獲取方法簽名后,判斷返回類型信息是否正確,再調(diào)用forwardInvocation執(zhí)行NSInvocation對象,并將結(jié)果返回。如果對象沒實(shí)現(xiàn)methodSignatureForSelector方法,繼續(xù)執(zhí)行后面方法。 - 3.調(diào)用
doesNotRecognizeSelector方法,拋出異常。
