曾經(jīng)覺得Objc特別方便上手诵闭,面對(duì)著 Cocoa 中大量 API顷霹,只知道簡(jiǎn)單的查文檔和調(diào)用咪惠。還記得初學(xué) Objective-C 時(shí)把[receiver message]
當(dāng)成簡(jiǎn)單的方法調(diào)用,而無視了“發(fā)送消息”這句話的深刻含義淋淀。其實(shí)[receiver message]
會(huì)被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector)
如果消息含有參數(shù)遥昧,則為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
如果消息的接收者能夠找到對(duì)應(yīng)的selector
,那么就相當(dāng)于直接執(zhí)行了接收者這個(gè)對(duì)象的特定方法绅喉;否則渠鸽,消息要么被轉(zhuǎn)發(fā),或是臨時(shí)向接收者動(dòng)態(tài)添加這個(gè)selector
對(duì)應(yīng)的實(shí)現(xiàn)內(nèi)容柴罐,要么就干脆玩完崩潰掉徽缚。
現(xiàn)在可以看出[receiver message]
真的不是一個(gè)簡(jiǎn)簡(jiǎn)單單的方法調(diào)用。因?yàn)檫@只是在編譯階段確定了要向接收者發(fā)送message
這條消息革屠,而receive
將要如何響應(yīng)這條消息凿试,那就要看運(yùn)行時(shí)發(fā)生的情況來決定了。
Objective-C 的 Runtime 鑄就了它動(dòng)態(tài)語言的特性似芝,這些深層次的知識(shí)雖然平時(shí)寫代碼用的少一些那婉,但是卻是每個(gè) Objc 程序員需要了解的。
SEL
objc_msgSend
函數(shù)第二個(gè)參數(shù)類型為SEL
,它是selector
在Objc中的表示類型(Swift中是Selector
類)。selector
是方法選擇器趴腋,可以理解為區(qū)分方法的 ID俏让,而這個(gè) ID 的數(shù)據(jù)結(jié)構(gòu)是SEL
:
typedef struct objc_selector *SEL;
其實(shí)它就是個(gè)映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()
或者 Runtime 系統(tǒng)的sel_registerName
函數(shù)來獲得一個(gè)SEL類型的方法選擇器市怎。不同類中相同名字的方法所對(duì)應(yīng)的方法選擇器是相同的鸯檬,即使方法名字相同而變量類型不同也會(huì)導(dǎo)致它們具有相同的方法選擇器伶选,于是 Objc 中方法命名有時(shí)會(huì)帶上參數(shù)類型
id
objc_msgSend
第一個(gè)參數(shù)類型為id
隐岛,大家對(duì)它都不陌生猫妙,它是一個(gè)指向類實(shí)例的指針:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object
結(jié)構(gòu)體包含一個(gè)isa
指針,根據(jù)isa
指針就可以順藤摸瓜找到對(duì)象所屬的類聚凹。
PS:isa
指針不總是指向?qū)嵗龑?duì)象所屬的類割坠,不能依靠它來確定類型,而是應(yīng)該用class方法來確定實(shí)例對(duì)象的類妒牙。因?yàn)?code>KVO的實(shí)現(xiàn)機(jī)理就是將被觀察對(duì)象的isa指針指向一個(gè)中間類而不是真實(shí)的類彼哼,這是一種叫做 isa-swizzling的技術(shù),詳見<a >官方文檔</a>
Class
之所以說isa
是指針是因?yàn)镃lass其實(shí)是一個(gè)指向objc_class
結(jié)構(gòu)體的指針:
typedef struct objc_class *Class;```
而`objc_class`就是我們摸到的那個(gè)瓜单旁,里面的東西多著呢:
```objectivec
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif} OBJC2_UNAVAILABLE;
可以看到運(yùn)行時(shí)一個(gè)類還關(guān)聯(lián)了它的超類指針沪羔,類名饥伊,成員變量象浑,方法,緩存琅豆,還有附屬的協(xié)議愉豺。
PS:OBJC2_UNAVAILABLE
之類的宏定義是蘋果在 Objc 中對(duì)系統(tǒng)運(yùn)行版本進(jìn)行約束的黑魔法,為的是兼容非Objective-C 2.0的遺留邏輯茫因,但我們?nèi)阅軓闹蝎@得一些有價(jià)值的信息蚪拦,有興趣的可以查看源代碼。
Objective-C 2.0 的頭文件雖然沒暴露出objc_class
結(jié)構(gòu)體更詳細(xì)的設(shè)計(jì)冻押,我們依然可以從Objective-C 1.0 的定義中小窺端倪:
在objc_class
結(jié)構(gòu)體中:ivars
是objc_ivar_list
指針驰贷;methodLists
是指向objc_method_list
指針的指針。也就是說可以動(dòng)態(tài)修改*methodLists
的值來添加成員方法洛巢,這也是Category實(shí)現(xiàn)的原理括袒,同樣解釋了Category不能添加屬性的原因。而最新版的 Runtime 源碼對(duì)這一塊的<a >描述</a>已經(jīng)有很大變化稿茉,可以參考下美團(tuán)技術(shù)團(tuán)隊(duì)的深入理解<a >Objective-C:Category</a>锹锰。
PS:任性的話可以在Category中添加@dynamic
的屬性,并利用運(yùn)行期動(dòng)態(tài)提供存取方法或干脆動(dòng)態(tài)轉(zhuǎn)發(fā)漓库;或者干脆使用關(guān)聯(lián)度對(duì)象(AssociatedObject)
其中objc_ivar_list
和objc_method_list
分別是成員變量列表和方法列表:
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
如果你C語言不是特別好恃慧,可以直接理解為objc_ivar_list結(jié)構(gòu)體存儲(chǔ)著objc_ivar數(shù)組列表,而objc_ivar結(jié)構(gòu)體存儲(chǔ)了類的單個(gè)成員變量的信息渺蒿;同理objc_method_list結(jié)構(gòu)體存儲(chǔ)著objc_method數(shù)組列表痢士,而objc_method結(jié)構(gòu)體存儲(chǔ)了類的某個(gè)方法的信息。
最后要提到的還有一個(gè)objc_cache茂装,顧名思義它是緩存怠蹂,它在objc_class的作用很重要陪汽,在后面會(huì)講到。
不知道你是否注意到了objc_class中也有一個(gè)isa對(duì)象褥蚯,這是因?yàn)橐粋€(gè) ObjC 類本身同時(shí)也是一個(gè)對(duì)象挚冤,為了處理類和對(duì)象的關(guān)系,runtime 庫創(chuàng)建了一種叫做元類 (Meta Class) 的東西赞庶,類對(duì)象所屬類型就叫做元類训挡,它用來表述類對(duì)象本身所具備的元數(shù)據(jù)。類方法就定義于此處歧强,因?yàn)檫@些方法可以理解成類對(duì)象的實(shí)例方法澜薄。每個(gè)類僅有一個(gè)類對(duì)象,而每個(gè)類對(duì)象僅有一個(gè)與之相關(guān)的元類摊册。當(dāng)你發(fā)出一個(gè)類似[NSObject alloc]的消息時(shí)肤京,你事實(shí)上是把這個(gè)消息發(fā)給了一個(gè)類對(duì)象 (Class Object) ,這個(gè)類對(duì)象必須是一個(gè)元類的實(shí)例茅特,而這個(gè)元類同時(shí)也是一個(gè)根元類 (root meta class) 的實(shí)例忘分。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應(yīng)消息的類方法白修。所以當(dāng) [NSObject alloc] 這條消息發(fā)給類對(duì)象的時(shí)候妒峦,objc_msgSend()會(huì)去它的元類里面去查找能夠響應(yīng)消息的方法,如果找到了兵睛,然后對(duì)這個(gè)類對(duì)象執(zhí)行方法調(diào)用肯骇。
上圖實(shí)線是super_class
指針,虛線是isa
指針祖很。 有趣的是根元類的超類是NSObject
笛丙,而isa
指向了自己,而NSObject
的超類為nil
假颇,也就是它沒有超類胚鸯。
Method
Method是一種代表類中的某個(gè)方法的類型。
typedef struct objc_method *Method;
而objc_method
在上面的方法列表中提到過拆融,它存儲(chǔ)了方法名蠢琳,方法類型和方法實(shí)現(xiàn):
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
- 方法名類型為
SEL
,前面提到過相同名字的方法即使在不同類中定義镜豹,它們的方法選擇器也相同傲须。 - 方法類型
method_types
是個(gè)char
指針,其實(shí)存儲(chǔ)著方法的參數(shù)類型和返回值類型趟脂。 -
method_imp
指向了方法的實(shí)現(xiàn)泰讽,本質(zhì)上是一個(gè)函數(shù)指針,后面會(huì)詳細(xì)講到。
Ivar
Ivar
是一種代表類中實(shí)例變量的類型已卸。
typedef struct objc_ivar *Ivar;
而objc_ivar
在上面的成員變量列表中也提到過:
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以根據(jù)實(shí)例查找其在類中的名字佛玄,也就是“反射”:
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}
class_copyIvarList
函數(shù)獲取的不僅有實(shí)例變量累澡,還有屬性梦抢。但會(huì)在原本的屬性名前加上一個(gè)下劃線。
IMP
IMP
在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個(gè)<a >函數(shù)指針</a>愧哟,這是由編譯器生成的奥吩。當(dāng)你發(fā)起一個(gè) ObjC
消息之后,最終它會(huì)執(zhí)行的那段代碼蕊梧,就是由這個(gè)函數(shù)指針指定的霞赫。而IMP
這個(gè)函數(shù)指針就指向了這個(gè)方法的實(shí)現(xiàn)。既然得到了執(zhí)行某個(gè)實(shí)例某個(gè)方法的入口肥矢,我們就可以繞開消息傳遞階段端衰,直接執(zhí)行方法。
你會(huì)發(fā)現(xiàn)IMP
指向的方法與objc_msgSend
函數(shù)類型相同甘改,參數(shù)都包含id
和SEL
類型旅东。每個(gè)方法名都對(duì)應(yīng)一個(gè)SEL
類型的方法選擇器,而每個(gè)實(shí)例對(duì)象中的SEL
對(duì)應(yīng)的方法實(shí)現(xiàn)肯定是唯一的楼誓,通過一組id
和SEL
參數(shù)就能確定唯一的方法實(shí)現(xiàn)地址玉锌;反之亦然名挥。
Cache
在runtime.h
中Cache
的定義如下:
typedef struct objc_cache *Cache
還記得之前objc_class
結(jié)構(gòu)體中有一個(gè)struct objc_cache *cache
吧疟羹,它到底是緩存啥的呢,先看看objc_cache
的實(shí)現(xiàn):
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache
為方法調(diào)用的性能進(jìn)行優(yōu)化禀倔,通俗地講榄融,每當(dāng)實(shí)例對(duì)象接收到一個(gè)消息時(shí),它不會(huì)直接在isa
指向的類的方法列表中遍歷查找能夠響應(yīng)消息的方法救湖,因?yàn)檫@樣效率太低了愧杯,而是優(yōu)先在Cache
中查找。Runtime
系統(tǒng)會(huì)把被調(diào)用的方法存到Cache
中(理論上講一個(gè)方法如果被調(diào)用鞋既,那么它有可能今后還會(huì)被調(diào)用)力九,下次查找的時(shí)候效率更高。這根計(jì)算機(jī)組成原理中學(xué)過的 CPU
繞過主存先訪問Cache
的道理挺像邑闺,而我猜蘋果為提高Cache
命中率應(yīng)該也做了努力吧跌前。
Property
@property
標(biāo)記了類中的屬性,這個(gè)不必多說大家都很熟悉陡舅,它是一個(gè)指向objc_property
結(jié)構(gòu)體的指針:
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個(gè)更常用
可以通過class_copyPropertyList
和protocol_copyPropertyList
方法來獲取類和協(xié)議中的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
返回類型為指向指針的指針抵乓,哈哈,因?yàn)閷傩粤斜硎莻€(gè)數(shù)組,每個(gè)元素內(nèi)容都是一個(gè)objc_property_t
指針灾炭,而這兩個(gè)函數(shù)返回的值是指向這個(gè)數(shù)組的指針茎芋。舉個(gè)例子,先聲明一個(gè)類:
interface Lender : NSObject {
float alone;
}
@property float alone;
@end
你可以用下面的代碼獲取屬性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以用property_getName
函數(shù)來查找屬性名稱:
const char *property_getName(objc_property_t property)
你可以用class_getProperty
和protocol_getProperty
通過給出的名稱來在類和協(xié)議中獲取屬性的引用:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用property_getAttributes
函數(shù)來發(fā)掘?qū)傩缘拿Q和@encode
類型字符串:
const char *property_getAttributes(objc_property_t property)
把上面的代碼放一起蜈出,你就能從一個(gè)類中獲取它的屬性啦:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
對(duì)比下class_copyIvarList
函數(shù)田弥,使用 class_copyPropertyList
函數(shù)只能獲取類的屬性,而不包含成員變量铡原。但此時(shí)獲取的屬性名是不帶下劃線的皱蹦。