Objective-C的runtime語言使它具備了動態(tài)語言的特性,也就是平時所說的“運行時”。在runtime的基礎(chǔ)上,可以做很多平時難以想到事,或者化簡原先 較為繁雜的解決方案。
相對于靜態(tài)語言,比如C的以下程序
#include
void run()
{}
int main()
{
return 0;
}
執(zhí)行clang -c進行編譯后,獲取符號表nm run.o,可以得到全局唯一的符號_run,對函數(shù)run的調(diào)用直接參考鏈接后_run符號在代碼段的地址
0000000000000010 T _main
0000000000000000 T _run
對比Objective-C的以下函數(shù)
@implementation Dog : NSObject
- (void)run
{}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc] init];
[dog run];
}
return 0;
}
執(zhí)行clang -rewrite-objc main.m將其轉(zhuǎn)換成底層C++文件后可以得到
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));
}
return 0;
}
可以看到,對Objective-C編譯前期,會將內(nèi)部的方法調(diào)用,轉(zhuǎn)換成調(diào)用objc_msgSend。也就是說,編譯完成后,方法地址是不能確定的,需要在運行時,通過Selector進行查找,而這正是runtime的關(guān)鍵,也就是發(fā)送消息機制。
runtime的基本要素
如上面例子所示,在編譯后[dog run]被編譯器轉(zhuǎn)化成了
((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));
// 假設(shè)能省略(void (*)(id, SEL))(void *)和id指針強轉(zhuǎn)[實際上還是需要的]
// sel_registerName表示注冊一個selector
objc_msgSend(dog, sel_registerName("run"));
將上面的情況抽取成統(tǒng)一的說法就是,在編譯器編譯后[receiver message]會被轉(zhuǎn)化成以下形式
objc_msgSend(receiver, selector)
objc_msgSend是一個消息發(fā)送函數(shù),它以消息接收者和方法名作為基礎(chǔ)參數(shù)。
在有參數(shù)的情況下,則會被轉(zhuǎn)換為
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息的接收者receiver在接受到消息后,查找對應(yīng)selector的實現(xiàn),根據(jù)查找的結(jié)果可以進行若干種種不同的處理。
更深層的了解,需要了解下對應(yīng)的數(shù)據(jù)結(jié)構(gòu)
id
上文中objc_msgSend的第一個參數(shù)有個強轉(zhuǎn)類型,即id。id是可以指向?qū)ο蟮娜f能指針,查看runtime源碼,得知其定義如下:
typedef struct objc_object *id;
// objc_object
struct objc_object {
private:
isa_t isa;
}
// isa_t
union isa_t
{
Class cls;
uintptr_t bits;
}
根據(jù)union聯(lián)合的存儲空間以大成員的存儲空間計算性質(zhì),可以猜測isa_t的作用只是真不同位數(shù)處理器的優(yōu)化,我們可以直接這樣表示:
struct objc_object {
private:
Class isa;
}
可以看出,id是一個指向objc_object結(jié)構(gòu)體的指針(注意,在runtime中對象可以用結(jié)構(gòu)體進行表示)。
objc_object結(jié)構(gòu)體包含了Class isa成員,而isa就是我們常說的創(chuàng)建一個對象時,用來指向所屬類的指針。因此根據(jù)isa就可以獲取對應(yīng)的類。
- 注:C++中結(jié)構(gòu)的作用被拓寬了,也表示定義一個類的類型,struct和class的區(qū)別就在默認類型上一個是public,一個是private,這里就直接描述為結(jié)構(gòu)體了
Class
上文中,isa為Class類型,而Class則是objc_class指針類型的別名:
typedef struct objc_class *Class;
而objc_class具體的定義如下:
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
}
// class_data_bits_t
struct class_data_bits_t {
...
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
...
}
// class_rw_t
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
union {
method_list_t **method_lists; // RW_METHOD_ARRAY == 1
method_list_t *method_list; // RW_METHOD_ARRAY == 0
};
struct chained_property_list *properties;
const protocol_list_t ** protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
// class_ro_t
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
const method_list_t * baseMethods;
const protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
const property_list_t *baseProperties;
};
在上文中已經(jīng)介紹過objc_object結(jié)構(gòu)體,objc_class繼承自結(jié)構(gòu)體objc_object??梢钥闯?code>objc_object的isa為private類型成員變量,objc_class繼承后無法訪問,所以objc_object提供了以下兩個成員函數(shù):
Class ISA();
// getIsa內(nèi)部調(diào)用ISA返回isa_t聯(lián)合中cls成員
Class getIsa();
所以,對objc_class重要的成員變量進行下解釋:
isa為指向?qū)ο髮?yīng)類的指針(這里注意一點,由于類也是一個對象(單例),所以這個單例中也有一個isa指針指向類對象所屬的類->metaClass,即元類)superclass為指向父類的指針cache用于對調(diào)用方法的緩存,類似CPU先訪問L1、L2、L3緩存的目的相似,它也是推斷最近調(diào)用的方法極有可能被二次調(diào)用,并將其存入cache,在二次調(diào)用時先在cache查找方法,而不是直接在類的方法列表中查找properties為屬性列表protocols為協(xié)議列表method_lists/method_list為方法列表ivars為成員變量列表-
class_ro_t結(jié)構(gòu)體中存儲的都是類基本的東西,比如獲取'load'方法時,是從baseMethods獲取相應(yīng)的IMP函數(shù)實現(xiàn)的:
IMP objc_class::getLoadMethod()
{
rwlock_assert_locked(&runtimeLock);
const method_list_t *mlist;
uint32_t i;
assert(isRealized());
assert(ISA()->isRealized());
assert(!isMetaClass());
assert(ISA()->isMetaClass());
mlist = ISA()->data()->ro->baseMethods;
if (mlist) {
for (i = 0; i < mlist->count; i++) {
method_t *m = method_list_nth(mlist, i);
const char *name = sel_cname(m->name);
if (0 == strcmp(name, "load")) {
return m->imp;
}
}
}
return nil;
}
其中先了解下`ivar_list_t`、`method_list_t`、`cache_t`的結(jié)構(gòu)定義:
`ivar_list_t`的結(jié)構(gòu)為:
- `ivar_t`就是對應(yīng)的成員變量
```objc
struct ivar_list_t {
uint32_t entsize;
uint32_t count;
ivar_t first;
};
method_list_t為:
- 其中
method_iterator為結(jié)構(gòu)體自己構(gòu)造的一個迭代器,用來訪問方法,可以看到,構(gòu)造的迭代器結(jié)構(gòu)體中包含了method成員變量
struct method_list_t {
uint32_t entsize_NEVER_USE; // high bits used for fixup markers
uint32_t count;
method_t first;
// iterate methods, taking entsize into account
// fixme need a proper const_iterator
struct method_iterator {
uint32_t entsize;
uint32_t index; // keeping track of this saves a divide in operator-
method_t* method;
...
}
cache_t為:
- 可以看出
bucket_t包含了一個IMP類型的私有成員,供查找后調(diào)用實現(xiàn) -
_occupied和_mask分別表示實際占用的緩存_buckets總數(shù)和分配的緩存_buckets總數(shù)
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
...
}
// bucket_t
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
...
}
上文還涉及到了一個概念metaClass元類,元類為類對象所屬的類,以實例解釋:
當(dāng)我們調(diào)用類方法時,消息的接收者即為類,如文中一開始的代碼:
Dog *dog = [[Dog alloc] init];
這里的alloc消息即發(fā)送給了Dog類,編譯轉(zhuǎn)換后的代碼為:
Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
我們只需要關(guān)注這一行:
- 這里獲取到的是類對象,只要再獲取一次就得到了元類
// objc_getClass表示根據(jù)對象名獲取對應(yīng)的類
objc_getClass("Dog")
// 獲取元類
objc_getClass(objc_getClass("Dog"))
關(guān)于元類,蘋果提供了這么一張表:

圖中的實線是
superclass指針,虛線是isa指針??梢钥吹剑惖某?code>NSObject(Root class)并沒有對應(yīng)的超類,并且,它的isa指針指向了自己。總結(jié)一下:
- 每個實例對象的
isa都指向了所屬的類 - 每個類對象的
isa都指向了所屬的類,即元類,其superclass指針指向繼承的父類 - 每個元類的
isa都指向了超類,即NSObject
Ivar
Ivar,我把它理解成instance variable,也就是實例變量,可以觀察它的定義:
typedef struct ivar_t *Ivar;
// ivar_t
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
// 內(nèi)存中數(shù)據(jù)對齊(如字對齊、半字對齊等)
uint32_t alignment() {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};
Ivar其實是指向ivar_t結(jié)構(gòu)體的指針,它包含了實例變量名(name)、類型(type)、相對對象地址偏移(offset)以及內(nèi)存數(shù)據(jù)對齊等信息。
跟多關(guān)于實例變量的剖析可以查看Objective-C類成員變量深度剖析
Method
從以下定義的結(jié)構(gòu)體可以看出,Method主要住用為關(guān)聯(lián)了方法名SEL和方法的實現(xiàn)IMP,當(dāng)遍通過Method自己的定義的迭代器查找方法名SEL時,就可以找到對應(yīng)的方法實現(xiàn)IMP,從而調(diào)用方法的實現(xiàn)執(zhí)行相關(guān)的操作。types表示方法實現(xiàn)的參數(shù)以及返回值類型。
typedef struct method_t *Method;
// method_t
struct method_t {
SEL name;
const char *types;
IMP imp;
...
}
SEL
SEL為方法選擇器,觀察下它的定義:
typedef struct objc_selector *SEL;
可以看出SEL實際是objc_selector指針類型的別名,它用于表示運行時方法的名字,以便進行方法實現(xiàn)的查找。因為要對應(yīng)方法實現(xiàn),所以每一個方法對應(yīng)的SEL都是唯一的。因此它不具備C++可以進行函數(shù)重載的特性,當(dāng)兩個方法名一樣時,會發(fā)生編譯錯誤,即使參數(shù)不一樣。
IMP
IMP的定義如下:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
可以看出IMP其實就是一個函數(shù)指針的別名,也可以把它理解為函數(shù)名。它有兩個必須的參數(shù):
-
id,為self指針,表示消息接收者 -
SEL,方法選擇器,表示一個方法的selector指針 - 后面的為傳送消息的一些參數(shù)
在某些情況下,通過獲取IMP而直接調(diào)用方法實現(xiàn),可以直接跳過消息傳遞機制,像C語言調(diào)用函數(shù)那樣,在一定程度上,可以提供程序的性能。
消息傳遞
了解完runtime中一些必要的元素,繼續(xù)回到文章開頭的代碼:
@implementation Dog : NSObject
- (void)run
{}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc] init];
[dog run];
}
return 0;
}
編譯器將其轉(zhuǎn)換成了:
- 為了看起來簡潔點,我把一些強制轉(zhuǎn)換變?yōu)閯e名
typedef (Dog *(*)(id, SEL))(void *) MyImp;
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Dog *dog = ((MyImp)objc_msgSend)((id)((MyImp)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
((MyImp)objc_msgSend)((id)dog, sel_registerName("run"));
}
return 0;
}
從上面的代碼可以看出,第二個objc_msgSend返回值是作為第一個objc_msgSend的首個參數(shù)的。
上文已經(jīng)說過,[receiver message]會被轉(zhuǎn)化成以下形式
objc_msgSend(receiver, selector, ...)
接下來看看它主要做了哪幾件事情:
- 根據(jù)
receiver的isa指針,獲取到所屬類,先在類的cache即緩存中查找selector,如果沒有找到,再在類的method_lists即方法列表中查找 - 如果沒有找到
selector,則會沿著下圖類的聯(lián)系路徑一直查找,直到NSObject類 - 如果找到了
selector,則獲取實現(xiàn)方法并調(diào)用,并傳入接收者對象以及方法的所有參數(shù);沒有找到時走方法解析和消息轉(zhuǎn)發(fā)流程。 - 將實現(xiàn)的返回值作為它自己的返回值

除此之外,
objc_msgSend還會傳遞兩個隱藏參數(shù):
- 消息接收對象(
self引用的對象) - 方法選擇器(
_cmd,調(diào)用的方法)
objc_msgSend找到方法實現(xiàn)后,會在調(diào)用該實現(xiàn)時,傳入這兩個隱藏參數(shù),這樣就能夠在方法實現(xiàn)里面里面獲取消息接受對象,即方法調(diào)用者了。
隱藏參數(shù)表示這兩個參數(shù)在源代碼方法的定義中并沒有聲明這兩個參數(shù),這兩個參數(shù)是在代碼編譯期間,被插入到實現(xiàn)中的。
self和super的聯(lián)系
根據(jù)上文對objc_msgSend的了解,可以解決以下代碼輸出一致問題
@implementation Dog : NSObject
- (void)run
{
NSLog(@"%@", [self class]);
NSLog(@"%@", [super class]);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc] init];
[dog run];
}
return 0;
}
輸出為:
[5491:173185] Dog
[5491:173185] Dog
這是為什么呢?先來看看編譯后的-run方法的情況:
static void _I_Dog_run(Dog * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_3f5nr6h10h1csn8byghy30q80000gn_T_main_d06ff4_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_3f5nr6h10h1csn8byghy30q80000gn_T_main_d06ff4_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }, sel_registerName("class")));
}
這里面只要關(guān)注兩句:
// [self class]
((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))
// [super class]
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }, sel_registerName("class"))
首先我們需要了解self和super的差異:
-
super:編譯標(biāo)識符,告訴編譯器,調(diào)用方法時,去調(diào)用父類的方法,而不是本類的方法 -
self:隱藏參數(shù),每個方法的實現(xiàn)第一個參數(shù)就是self
這里可以看出,編譯后,經(jīng)過super標(biāo)識符修飾的方法調(diào)用,會調(diào)用objc_msgSendSuper函數(shù)來進行消息的發(fā)送,而不是objc_msgSend。先來了解下objc_msgSendSuper的聲明:
id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );
其中objc_super的定義為:
// receiver 消息實際接收者
// class 指向當(dāng)前類的父類
struct objc_super { id receiver; Class class; };
結(jié)合以上信息,我們可以知道:
(__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }
就是對結(jié)構(gòu)體objc_super的賦值,也就是說objc_super->receiver=self。到這里可能就有點明了了,super只是告訴編譯器,去查找父類中的class方法,當(dāng)找到之后,使用objc_super->receiver即self進行調(diào)用。用流程表示就是:
[super class]->objc_msgSendSuper(objc_super{self, superclass)}, sel_registerName("class"))->objc_msgSend(objc_super->self, sel_registerName("class"))=[self class]。
可以看出兩者輸出結(jié)果一致的關(guān)鍵就是,[self class]的消息接收者和[super class]的消息接收者一樣,都是調(diào)用方法的實例對象。
方法解析和消息轉(zhuǎn)發(fā)
當(dāng)上文objc_msgSend處理流程中,selector沒有找到時,會觸發(fā)三個階段,在這三個階段都可以進行相關(guān)處理使程序不拋出異常:
- Method Resolution (動態(tài)方法解析)
- Fast Forwarding (備用接收者)
- Normal Forwarding (完整轉(zhuǎn)發(fā))
由于實際代碼中很少有看到這種操作,所以這里不做詳細解釋,參考這個資料即可Objective-C Runtime 運行時之三:方法與消息