
一. runtime簡介
runtime簡稱運(yùn)行時,是一套底層的 C 語言 API。OC就是運(yùn)行時機(jī)制,運(yùn)行時機(jī)制中最主要的是消息機(jī)制。而消息機(jī)制就是開發(fā)者在編碼過程中,可以給任意一個對象發(fā)送消息,在編譯階段只是確定了要向接收者發(fā)送這條消息,而接受者將要如何響應(yīng)和處理這條消息,那就要看運(yùn)行時來決定了。
- 對于C語言,函數(shù)的調(diào)用在編譯的時候會決定調(diào)用哪個函數(shù)。
- 對于OC的函數(shù),屬于動態(tài)調(diào)用過程,在編譯的時候并不能決定真正調(diào)用哪個函數(shù),只有在真正運(yùn)行的時候才會根據(jù)函數(shù)的名稱找到對應(yīng)的函數(shù)來調(diào)用。OC作為動態(tài)語言,它不僅需要一個編譯器,也需要一個運(yùn)行時系統(tǒng)來動態(tài)得創(chuàng)建類和對象、進(jìn)行消息傳遞和轉(zhuǎn)發(fā)。
- 事實(shí)證明:
在編譯階段,OC可以調(diào)用任何函數(shù),即使這個函數(shù)并未實(shí)現(xiàn),只要聲明過就不會報錯。
在編譯階段,C語言調(diào)用未實(shí)現(xiàn)的函數(shù)就會報錯。
二. runtime的數(shù)據(jù)結(jié)構(gòu)
源碼:
typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
通過以上源碼可以總結(jié)出以下幾點(diǎn):
- objc_class被typedef成了Class類型,objc_object被typedef成了id類型。
- object類和NSObject類里面分別都包含一個objc_class類型的isa。
- objc_class和objc_object都是結(jié)構(gòu)體,且objc_class繼承于objc_object,因此Objective-C 中類也是一個對象,叫類對象。
- objc_object包含一個isa_t類型的結(jié)構(gòu)體,因此objc_class也會包含isa_t類型的結(jié)構(gòu)體isa。
- 在objc_class中,除了isa之外,還有3個成員變量,一個是父類的指針,一個是方法緩存,最后一個這個類的實(shí)例方法鏈表。

isa

isa可分為兩類:
- 指針型isa:其值代表Class地址。
- 非指針型isa:其值的部分代表Class的地址,64位的其他部分留作他用,為了節(jié)省內(nèi)存。
isa指向:
- 關(guān)于對象,其isa指向類對象,實(shí)例對象調(diào)用方法就是通過isa找到類對象,到類對象中找到方法進(jìn)行調(diào)用。
- 關(guān)于類對象,其isa指向元類對象,調(diào)用類方法就是通過isa找到元類對象,到元類對象中找到方法進(jìn)行調(diào)用。
cache_t
源碼:
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
typedef unsigned int uint32_t;
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
cache_t的數(shù)據(jù)結(jié)構(gòu)如下:

它包括:
- _buckets,一個散列表,用來存儲Method的鏈表。
- mask: 分配用來緩存bucket的總數(shù)。
- occupied:表明目前實(shí)際占用的緩存bucket的個數(shù)。
而bucket_t的數(shù)據(jù)結(jié)構(gòu)是:
- key: cache_key_t。
- IMP:無類型的函數(shù)指針, 指向了一個方法的具體實(shí)現(xiàn)。

cache_t的意義在于:
- 用于快速查找方法執(zhí)行函數(shù),比如在調(diào)用方法時,如果方法有緩存,那就不用到方法列表中逐一遍歷去查找方法的具體實(shí)現(xiàn)了。
- 是可以增量擴(kuò)展的哈希表結(jié)構(gòu),可增量擴(kuò)展意思是當(dāng)存儲的數(shù)據(jù)量大時可以擴(kuò)展內(nèi)存空間來支持更多的緩存。
- 是局部性原理的最佳應(yīng)用:局部性原理體現(xiàn)在將調(diào)用頻次最高的方法放到緩存當(dāng)中,那么下次的命中率會更高,優(yōu)化方法調(diào)用的性能。往往當(dāng)一個實(shí)例對象調(diào)用方法時,首先根據(jù)對象的isa指針查找到它對應(yīng)的類對象,然后在類對象方法列表中搜索方法,如果沒有找到,就使用super_class指針到父類對象的方法列表中查找,一旦找到就調(diào)用方法,如果沒有找到,有可能消息轉(zhuǎn)發(fā),也可能忽略它。但這樣查找方式效率太低,因?yàn)橥粋€類大概只有20%的方法經(jīng)常被調(diào)用,占總調(diào)用次數(shù)的80%。所以使用Cache來緩存經(jīng)常調(diào)用的方法,當(dāng)調(diào)用方法時,優(yōu)先在Cache查找,如果沒有找到,再到方法列表中查找,這樣就能優(yōu)化方法調(diào)用的性能。
class_data_bits_t
class_data_bits_t數(shù)據(jù)結(jié)構(gòu):

總結(jié):
- class_data_bits_t結(jié)構(gòu)體是對class_rw_t的封裝,代表著類或分類中變量,屬性,方法鏈表。
- class_rw_t結(jié)構(gòu)體又是對class_ro_t的封裝,它的結(jié)構(gòu)包括:class_ro_t,protocols,properties,methods,后三者是二維數(shù)組,比如methods的元素是一個類或者它的分類的方法列表methodList,而methodList的元素是method_t。
- class_ro_t結(jié)構(gòu)體是一個指向常量的指針,ro代表它是只讀的,
存儲編譯器決定了的成員變量,屬性,方法和遵守協(xié)議,而它們都是一維數(shù)組。
method_t
源碼:
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
從源碼可以看出method_t結(jié)構(gòu)體有三個成員變量:
- SEL name:代表方法名稱。
- const char* types:返回值和參數(shù)組合,不可變的字符類型指針。
- IMP imp:無類型的函數(shù)指針,對應(yīng)的是函數(shù)體。
其中types的數(shù)據(jù)結(jié)構(gòu)如下:

我們知道runtime的messageSend方法頭兩個參數(shù)是默認(rèn)固定的參數(shù),第一個參數(shù)默認(rèn)是self,第二個參數(shù)默認(rèn)是方法名稱SEL,所以types值中代表參數(shù)的兩個固定的值是@和:,比如方法- (void)aMethod;,它的types值就是:v@:,v代表返回值是void,@代表第一個參數(shù)是id類型的self,:代表第二個參數(shù)SEL。

總結(jié)
runtime整個數(shù)據(jù)結(jié)構(gòu)如下:

三. 消息傳遞
實(shí)例對象、類對象、元類對象
先看一張經(jīng)典的圖,它能展示出三者之間的關(guān)系:

總結(jié):
- 類對象存儲實(shí)例方法列表等信息。
- 元類對象存儲類方法列表等信息。
- 類對象和元類對象都是objc_class數(shù)據(jù)結(jié)構(gòu)類型的,都繼承于objc_object, 所以都有isa指針,所以實(shí)例對象可以通過isa找到類對象,進(jìn)而訪問類對象存儲的實(shí)例方法列表,類對象也可以通過isa找到元類對象,進(jìn)而訪問元類對象存儲的類方法列表。
實(shí)例對象調(diào)用實(shí)例方法過程
圖解過程:

在此過程中會經(jīng)歷三種查找:
- 緩存查找:根據(jù)給定的SEL,通過哈希函數(shù)的算法得到bucket_t對應(yīng)cache_t數(shù)組中的索引位置,哈希函數(shù)表達(dá)式就是SEL選擇器因子和對應(yīng)的mask作與操作,mask是cache_t的成員變量,查找到bucket_t后,就能提取到IMP指針,返回給調(diào)用方。

- 在當(dāng)前類中查找:
- 對于已排序好的方法列表,采用二分查找算法查找方法對應(yīng)執(zhí)行函數(shù)。
- 對于沒有排序的方法列表,采用一般遍歷查找方法對應(yīng)執(zhí)行函數(shù)。
-
父類逐級查找:通過當(dāng)前類的superClass指針去訪問父類,先判斷父類是否為空,是空就結(jié)束查找,否就看能否根據(jù)SEL在父類緩存中找到相應(yīng)的方法實(shí)現(xiàn),如果找到就結(jié)束流程,如果父類緩存中沒有,就遍歷父類方法列表,看看能否查找到SEL對應(yīng)的方法實(shí)現(xiàn),如果還沒有,就根據(jù)父類的superClass找到父類的父類,繼續(xù)此逐級查找過程。
所以整個消息傳遞流程可以作如下總結(jié):
- 當(dāng)實(shí)例對象調(diào)用一個方法時,系統(tǒng)會根據(jù)實(shí)例對象的isa指針找到它的類對象,查找類對象的緩存中是否有對應(yīng)SEL的IMP方法實(shí)現(xiàn),如果沒有,則在當(dāng)前類對象的實(shí)例方法列表,二分查找或遍歷查找同名的方法實(shí)現(xiàn)IMP,如果找到,填充到緩存中,并返回selector,如果沒有找到,根據(jù)類對象的superClass指針到父類對象的方法列表中進(jìn)行父類逐級查找,直到根類對象還沒找到,則進(jìn)入消息轉(zhuǎn)發(fā)流程。
- 當(dāng)調(diào)用一個類方法時,類對象根據(jù)isa指針找到元類對象,也是先到元類對象的緩存中查找,沒找到則在當(dāng)前元類對象的類方法列表,二分查找或遍歷查找同名的方法實(shí)現(xiàn)IMP,如果找到,填充到緩存中,并返回selector,如果沒有找到,根據(jù)元類對象的superClass指針到父元類對象的方法列表中進(jìn)行父類逐級查找,直到根元類對象,如果還沒找到,就到根元類對象的superClass指向的根類對象(也就是NSObject)中找同名的方法,如果還沒找到就走消息轉(zhuǎn)發(fā)流程。
四. 消息轉(zhuǎn)發(fā)機(jī)制
我們定義的方法如果沒有實(shí)現(xiàn),系統(tǒng)會依次調(diào)用以下方法:
- (void)testMethod;
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(testMethod)) {
NSLog(@"resolveInstanceMethod:");
return NO;
} else {
return [super resolveInstanceMethod:sel];
}
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector:(");
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(testMethod)) {
NSLog(@"methodSignatureForSelector:");
return [NSMethodSignature signatureWithObjCTypes: "v@:"];
} else {
return [super methodSignatureForSelector:aSelector];
}
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation:");
}
整個實(shí)例方法的消息轉(zhuǎn)發(fā)流程:
-
resolveInstanceMethod方法如果返回YES,那么根據(jù)SEL就找到了對應(yīng)的函數(shù)實(shí)現(xiàn),代表消息已處理;如果返回NO,系統(tǒng)會給出第二次機(jī)會去處理消息。
比如,我們給原來定義的方法動態(tài)添加一個實(shí)現(xiàn):
void testImp (void) {
NSLog(@"hhh");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(testMethod)) {
NSLog(@"resolveInstanceMethod:");
//動態(tài)添加方法實(shí)現(xiàn)
class_addMethod(self, @selector(testMethod), testImp, "v@:");
return YES;
} else {
return [super resolveInstanceMethod:sel];
}
}
那么根據(jù)testMethod方法的SEL就找到了testImp的實(shí)現(xiàn),因此就不會再調(diào)用下面的方法,消息轉(zhuǎn)發(fā)結(jié)束。
-
forwardingTargetForSelector方法用于指定一個轉(zhuǎn)發(fā)目標(biāo),系統(tǒng)會把消息轉(zhuǎn)發(fā)給此目標(biāo)。如果我們在此函數(shù)中返回一個nil,也就是沒有指定轉(zhuǎn)發(fā)目標(biāo)的話,系統(tǒng)會給出第三次處理這條消息的機(jī)會。 -
methodSignatureForSelector方法的返回值是個NSMethodSignature對象,它是對方法的返回值,返回值類型,參數(shù),參數(shù)類型,參數(shù)個數(shù)的封裝,如果此函數(shù)返回了方法簽名,則系統(tǒng)會接著調(diào)用forwardInvocation方法,如果該方法能處理消息,則消息轉(zhuǎn)發(fā)流程就此結(jié)束,如果不能處理或methodSignatureForSelector沒有返回方法簽名,則此消息就被標(biāo)記為無法處理。
消息轉(zhuǎn)發(fā)流程圖解:

五. method-Swizzling
將兩個方法對應(yīng)的方法實(shí)現(xiàn)進(jìn)行交換:

使用場景:比如統(tǒng)計頁面進(jìn)入次數(shù)或時長,那么可以交換系統(tǒng)的viewWillAppear方法,在新方法中插入統(tǒng)計邏輯即可,而不用在每個頁面的viewWillAppear方法中都作統(tǒng)計。
#import "UIViewController+runtime.h"
#import <objc/runtime.h>
@implementation UIViewController (runtime)
+ (void)load {
Method testMethod = class_getInstanceMethod([UIViewController class], @selector(viewWillAppear:));
Method testOtherMethod = class_getInstanceMethod(self, @selector(testOtherMethod));
method_exchangeImplementations(testMethod, testOtherMethod);
}
- (void)testOtherMethod {
//插入邏輯
NSLog(@"testOtherMethod");
}
六. 動態(tài)方法解析
動態(tài)方法解析總結(jié):
- 用@dynamic標(biāo)記屬性,則屬性的get,set方法就是運(yùn)行時添加的而不是編譯時聲明好的。
- 編譯時語言和動態(tài)運(yùn)行時語言的區(qū)別是:
- 動態(tài)運(yùn)行時語言將函數(shù)決議推遲到運(yùn)行時,為方法添加具體的實(shí)現(xiàn)。比如當(dāng)我們把一個屬性標(biāo)示為@dynamic時,代表著不需要編譯器在編譯時為屬性生成get,set方法實(shí)現(xiàn),而是在運(yùn)行時具體調(diào)用屬性的get或set方法時,再添加相應(yīng)的實(shí)現(xiàn)。
- 編譯時語言在編譯期進(jìn)行函數(shù)決議,也就是在編譯期間就確定了一個方法名稱對應(yīng)的具體的方法實(shí)現(xiàn),而在具體的運(yùn)行中是不能修改的。
方法緩存
方法緩存的目的:優(yōu)化方法查找性能,因?yàn)楫?dāng)一個方法在比較“上層”的類中,用比較“下層”(繼承關(guān)系上的上下層)對象去調(diào)用的時候,如果沒有緩存,那么整個查找鏈?zhǔn)窍喈?dāng)長的。就算方法是在這個類里面,當(dāng)方法比較多的時候,每次都查找也是費(fèi)事費(fèi)力的一件事情。所以將調(diào)用頻率高的方法緩存下來,提高命中率,也是上面文章所說的局部性原理的最佳應(yīng)用。
推薦美團(tuán)技術(shù)團(tuán)隊的這篇文章: 深入理解 Objective-C:方法緩存。
關(guān)于runtime的面試題
1. [self class] 和 [super class]。
#import "Animal.h"
@interface Dog: Animal
@end
@implementation Dog
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
self和super的區(qū)別:
- self是類的一個隱藏參數(shù),每個方法的實(shí)現(xiàn)的第一個參數(shù)是self。
- super是一個”編譯器標(biāo)示符”,而不是隱藏參數(shù),它負(fù)責(zé)告訴編譯器,當(dāng)調(diào)用方法時,去調(diào)用父類的方法,而不是本類中的方法。
在調(diào)用[super class]的時候,runtime會去調(diào)用objc_msgSendSuper方法。
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
從源碼可以看出:
- objc_msgSendSuper方法中,第一個參數(shù)是一個objc_super結(jié)構(gòu)體。
- objc_super結(jié)構(gòu)體有兩個變量,一個是接收消息的receiver,一個是當(dāng)前類的父類super_class。
調(diào)用objc_msgSendSuper的流程是:
從objc_super結(jié)構(gòu)體中的superClass指向的父類的方法列表開始查找selector,找到后以objc_super->receiver去調(diào)用父類的這個selector。所以最后的調(diào)用者是objc->receiver也就是self,即objc_super->receiver = self。
最后結(jié)論:
無論是self還是super,接收者都是當(dāng)前對象,區(qū)別是self調(diào)用class時,是從該實(shí)例對象的類對象順次向上查找,而super調(diào)用class時,越過了該實(shí)例對象的類對象,是從其類對象的父類對象順次向上查找,不過最終都找到了根類對象NSObject中的class方法,從而返回相同的結(jié)果。
2. isKindOfClass 與 isMemberOfClass。
BOOL result1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL result2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL result3 = [(id)[Dog class] isKindOfClass:[Dog class]];
BOOL result4 = [(id)[Dog class] isMemberOfClass:[Dog class]];
NSLog(@"%d %d %d %d", result1, result2, result3, result4);
在isKindOfClass中有一個循環(huán),先判斷class是否等于meta class,不等就繼續(xù)循環(huán)判斷是否等于super class,不等再繼續(xù)取super class,如此循環(huán)下去。
[NSObject class]執(zhí)行完之后調(diào)用isKindOfClass,第一次判斷先判斷NSObject 和 NSObject的meta class是否相等,很明顯不等。接著第二次循環(huán)判斷NSObject與meta class的superclass是否相等,而Root class(meta) 的superclass 就是 Root class(class),也就是NSObject本身,所以相等,result1為YES。
同理,[Dog class]執(zhí)行完之后調(diào)用isKindOfClass,第一次for循環(huán),Dog的meta Class與Dog不等,第二次循環(huán),Dog meta Class的super class 指向的是 NSObject meta Class, 和 Dog也不相等。第三次循環(huán),NSObject Meta Class的super class指向的是NSObject Class,和 Dog 不相等。第四次循環(huán),NSObject Class 的super class 指向 nil, 和 Dog不相等,最終result3為NO。
如果是Dog的實(shí)例對象,[dog isKindOfClass:[Dog class],那么此時就應(yīng)該輸出YES了。因?yàn)樵趇sKindOfClass函數(shù)中,判斷Dog的isa指向是否是自己的類Dog,第一次for循環(huán)就能輸出YES了。
isMemberOfClass是拿到自己的isa指針和自己比較,是否相等。
第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject不相等。第四行,isa指向Dog的Meta Class,和Dog也不等,所以第二行result2和第四行result4都為NO。
