深入淺出ObjC之消息
羅朝輝 (http://ww.cppblog.com/kesalin)
轉(zhuǎn)載請(qǐng)注明出處
在入門級(jí)別的ObjC 教程中,我們常對(duì)從C++或Java 或其他面向?qū)ο笳Z(yǔ)言轉(zhuǎn)過(guò)來(lái)的程序員說(shuō),ObjC 中的方法調(diào)用(ObjC中的術(shù)語(yǔ)為消息)跟其他語(yǔ)言中的方法調(diào)用差不多,只是形式有些不同而已。
譬如C++ 中的:
Bird * aBird = new Bird(); aBird->fly(); |
在ObjC 中則如下:
Bird * aBird = [[Bird alloc] init]; [aBird fly]; |
乍看起來(lái),好像只是書寫形式不同而已,實(shí)則差異大矣。C++中的方法調(diào)用可能是動(dòng)態(tài)的,也可能是靜態(tài)的;而ObjC中的消息都為動(dòng)態(tài)的。下文將詳細(xì)介紹為什么是動(dòng)態(tài)的,以及編譯器在這背后做了些什么事情。
要說(shuō)清楚消息這個(gè)話題,我們必須先來(lái)了解三個(gè)概念Class, SEL, IMP,它們?cè)趏bjc/objc.h 中定義:
typedef struct objc_class *Class; typedef struct objc_object { Class isa; } *id; typedef struct objc_selector *SEL; typedef id (*IMP)(id, SEL, ...); |
Class 的含義
Class 被定義為一個(gè)指向 objc_class的結(jié)構(gòu)體指針,這個(gè)結(jié)構(gòu)體表示每一個(gè)類的類結(jié)構(gòu)。而 objc_class 在objc/objc_class.h中定義如下:
struct objc_class { struct objc_class super_class; /*父類*/ const char *name; /*類名字*/ long version; /*版本信息*/ long info; /*類信息*/ long instance_size; /*實(shí)例大小*/ struct objc_ivar_list *ivars; /*實(shí)例參數(shù)鏈表*/ struct objc_method_list **methodLists; /*方法鏈表*/ struct objc_cache *cache; /*方法緩存*/ struct objc_protocol_list *protocols; /*協(xié)議鏈表*/ }; |
由此可見(jiàn),Class 是指向類結(jié)構(gòu)體的指針,該類結(jié)構(gòu)體含有一個(gè)指向其父類類結(jié)構(gòu)的指針,該類方法的鏈表,該類方法的緩存以及其他必要信息。
NSObject 的class 方法就返回這樣一個(gè)指向其類結(jié)構(gòu)的指針。每一個(gè)類實(shí)例對(duì)象的第一個(gè)實(shí)例變量是一個(gè)指向該對(duì)象的類結(jié)構(gòu)的指針,叫做isa。通過(guò)該指針,對(duì)象可以訪問(wèn)它對(duì)應(yīng)的類以及相應(yīng)的父類。如圖一所示:

如圖一所示,圓形所代表的實(shí)例對(duì)象的第一個(gè)實(shí)例變量為 isa,它指向該類的類結(jié)構(gòu) The object’s class。而該類結(jié)構(gòu)有一個(gè)指向其父類類結(jié)構(gòu)的指針superclass, 以及自身消息名稱(selector)/實(shí)現(xiàn)地址(address)的方法鏈表。
方法的含義:
注意這里所說(shuō)的方法鏈表里面存儲(chǔ)的是Method 類型的。圖一中selector 就是指 Method的 SEL, address就是指Method的 IMP。
一個(gè)方法 Method,其包含一個(gè)方法選標(biāo) SEL – 表示該方法的名稱,一個(gè)types – 表示該方法參數(shù)的類型,一個(gè) IMP - 指向該方法的具體實(shí)現(xiàn)的函數(shù)指針。 Method 在頭文件 objc_class.h中定義如下:
typedef struct objc_method *Method;
typedef struct objc_ method { SEL method_name; char *method_types; IMP method_imp; }; |
SEL 的含義:
在前面我們看到方法選標(biāo) SEL 的定義為:
typedef struct objc_selector *SEL;
它是一個(gè)指向 objc_selector 指針,表示方法的名字/簽名。如下所示,打印出 selector。
-(NSInteger)maxIn:(NSInteger)a theOther:(NSInteger)b { return (a > b) ? a : b; } NSLog(@"SEL=%s", @selector(maxIn:theOther:)); 輸出:SEL=maxIn:theOther: |
不同的類可以擁有相同的 selector,這個(gè)沒(méi)有問(wèn)題,因?yàn)椴煌惖膶?shí)例對(duì)象performSelector相同的 selector 時(shí),會(huì)在各自的消息選標(biāo)(selector)/實(shí)現(xiàn)地址(address) 方法鏈表中根據(jù) selector 去查找具體的方法實(shí)現(xiàn)IMP, 然后用這個(gè)方法實(shí)現(xiàn)去執(zhí)行具體的實(shí)現(xiàn)代碼。這是一個(gè)動(dòng)態(tài)綁定的過(guò)程,在編譯的時(shí)候,我們不知道最終會(huì)執(zhí)行哪一些代碼,只有在執(zhí)行的時(shí)候,通過(guò)selector去查詢,我們才能確定具體的執(zhí)行代碼。
IMP 的含義:
在前面我們也看到 IMP 的定義為:
typedef id (*IMP)(id, SEL, ...);
根據(jù)前面id 的定義,我們知道 id是一個(gè)指向 objc_object 結(jié)構(gòu)體的指針,該結(jié)構(gòu)體只有一個(gè)成員isa,所以任何繼承自 NSObject 的類對(duì)象都可以用id 來(lái)指代,因?yàn)?NSObject 的第一個(gè)成員實(shí)例就是isa。
至此,我們就很清楚地知道 IMP 的含義:IMP 是一個(gè)函數(shù)指針,這個(gè)被指向的函數(shù)包含一個(gè)接收消息的對(duì)象id(self 指針), 調(diào)用方法的選標(biāo) SEL (方法名),以及不定個(gè)數(shù)的方法參數(shù),并返回一個(gè)id。也就是說(shuō) IMP 是消息最終調(diào)用的執(zhí)行代碼,是方法真正的實(shí)現(xiàn)代碼 。我們可以像在C語(yǔ)言里面一樣使用這個(gè)函數(shù)指針。
NSObject 類中的methodForSelector:方法就是這樣一個(gè)獲取指向方法實(shí)現(xiàn)IMP 的指針,methodForSelector:返回的指針和賦值的變量類型必須完全一致,包括方法的參數(shù)類型和返回值類型。
下面的例子展示了怎么使用指針來(lái)調(diào)用setFilled:的方法實(shí)現(xiàn):
void (*setter)(id, SEL, BOOL); int i; setter = (void(*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for (i = 0; i < 1000; i++) setter(targetList[i], @selector(setFilled:), YES); |
使用methodForSelector:來(lái)避免動(dòng)態(tài)綁定將減少大部分消息的開(kāi)銷,但是這只有在指定的消息被重復(fù)發(fā)送很多次時(shí)才有意義,例如上面的for循環(huán)。
注意,methodForSelector:是Cocoa運(yùn)行時(shí)系統(tǒng)的提供的功能,而不是Objective-C語(yǔ)言本身的功能。
消息調(diào)用過(guò)程:
至此我們對(duì)ObjC 中的消息應(yīng)該有個(gè)大致思路了:示例
Bird * aBird = [[Bird alloc] init]; [aBird fly]; |
中對(duì) fly 的調(diào)用,編譯器通過(guò)插入一些代碼,將之轉(zhuǎn)換為對(duì)方法具體實(shí)現(xiàn)IMP的調(diào)用,這個(gè) IMP是通過(guò)在 Bird 的類結(jié)構(gòu)中的方法鏈表中查找名稱為fly 的 選標(biāo)SEL 對(duì)應(yīng)的具體方法實(shí)現(xiàn)找到的。
上面的思路還有一些沒(méi)有提及的話題,比如說(shuō)編譯器插入了什么代碼,如果在方法鏈表中沒(méi)有找到對(duì)應(yīng)的 IMP又會(huì)如何,這些話題在下面展開(kāi)。
消息函數(shù) obj_msgSend:
編譯器會(huì)將消息轉(zhuǎn)換為對(duì)消息函數(shù) objc_msgSend的調(diào)用,該函數(shù)有兩個(gè)主要的參數(shù):消息接收者id 和消息對(duì)應(yīng)的方法選標(biāo) SEL, 同時(shí)接收消息中的任意參數(shù):
id objc_msgSend(id theReceiver, SELtheSelector, ...)
如上面的消息 [aBird fly]會(huì)被轉(zhuǎn)換為如下形式的函數(shù)調(diào)用:
objc_msgSend(aBird, @selector(fly));
該消息函數(shù)做了動(dòng)態(tài)綁定所需要的一切工作:
1,它首先找到 SEL 對(duì)應(yīng)的方法實(shí)現(xiàn) IMP。因?yàn)椴煌念悓?duì)同一方法可能會(huì)有不同的實(shí)現(xiàn),所以找到的方法實(shí)現(xiàn)依賴于消息接收者的類型。
2, 然后將消息接收者對(duì)象(指向消息接收者對(duì)象的指針)以及方法中指定的參數(shù)傳遞給方法實(shí)現(xiàn) IMP。
3, 最后,將方法實(shí)現(xiàn)的返回值作為該函數(shù)的返回值返回。
編譯器會(huì)自動(dòng)插入調(diào)用該消息函數(shù)objc_msgSend的代碼,我們無(wú)須在代碼中顯示調(diào)用該消息函數(shù)。當(dāng)objc_msgSend找到方法對(duì)應(yīng)的實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實(shí)現(xiàn),同時(shí),它還將傳遞兩個(gè)隱藏的參數(shù):消息的接收者以及方法名稱 SEL。這些參數(shù)幫助方法實(shí)現(xiàn)獲得了消息表達(dá)式的信息。它們被認(rèn)為是”隱藏“的是因?yàn)樗鼈儾](méi)有在定義方法的源代碼中聲明,而是在代碼編譯時(shí)是插入方法的實(shí)現(xiàn)中的。
盡管這些參數(shù)沒(méi)有被顯示聲明,但在源代碼中仍然可以引用它們(就象可以引用消息接收者對(duì)象的實(shí)例變量一樣)。在方法中可以通過(guò)self來(lái)引用消息接收者對(duì)象,通過(guò)選標(biāo)_cmd來(lái)引用方法本身。在下面的例子中,_cmd 指的是strange方法,self指的收到strange消息的對(duì)象。
- strange { id target = getTheReceiver(); SEL method = getTheMethod(); if (target == self || mothod == _cmd) return nil; return [target performSelector:method]; } |
在這兩個(gè)參數(shù)中,self更有用一些。實(shí)際上,它是在方法實(shí)現(xiàn)中訪問(wèn)消息接收者對(duì)象的實(shí)例變量的途徑。
查找 IMP 的過(guò)程:
前面說(shuō)了,objc_msgSend 會(huì)根據(jù)方法選標(biāo) SEL 在類結(jié)構(gòu)的方法列表中查找方法實(shí)現(xiàn)IMP。這里頭有一些文章,我們?cè)谇懊娴念惤Y(jié)構(gòu)中也看到有一個(gè)叫objc_cache *cache 的成員,這個(gè)緩存為提高效率而存在的。每個(gè)類都有一個(gè)獨(dú)立的緩存,同時(shí)包括繼承的方法和在該類中定義的方法。。
下面來(lái)剖析一段蘋果官方的源碼:
static Method look_up_method(Class cls, SEL sel, BOOL withCache, BOOL withResolver)
{
Method meth = NULL;
if (withCache) {
meth = _cache_getMethod(cls, sel, &_objc_msgForward_internal);
if (meth == (Method)1) {
// Cache contains forward:: . Stop searching.
return NULL;
}
}
if (!meth) meth = _class_getMethod(cls, sel);
if (!meth && withResolver) meth = _class_resolveMethod(cls, sel);
return meth;
}
通過(guò)分析上面的代碼,可以看到,查找時(shí):
1,首先去該類的方法 cache 中查找,如果找到了就返回它;
2,如果沒(méi)有找到,就去該類的方法列表中查找。如果在該類的方法列表中找到了,則將 IMP 返回,并將它加入cache中緩存起來(lái)。根據(jù)最近使用原則,這個(gè)方法再次調(diào)用的可能性很大,緩存起來(lái)可以節(jié)省下次調(diào)用再次查找的開(kāi)銷。3,3,如果在該類的方法列表中沒(méi)找到對(duì)應(yīng)的 IMP,在通過(guò)該類結(jié)構(gòu)中的 super_class指針在其父類結(jié)構(gòu)的方法列表中去查找,直到在某個(gè)父類的方法列表中找到對(duì)應(yīng)的IMP,返回它,并加入cache中;
4,如果在自身以及所有父類的方法列表中都沒(méi)有找到對(duì)應(yīng)的 IMP,則看是不是可以進(jìn)行動(dòng)態(tài)方法決議(后面有專文講述這個(gè)話題);
5,如果動(dòng)態(tài)方法決議沒(méi)能解決問(wèn)題,進(jìn)入下面要講的消息轉(zhuǎn)發(fā)流程。
便利函數(shù):
我們可以通過(guò)NSObject的一些方法獲取運(yùn)行時(shí)信息或動(dòng)態(tài)執(zhí)行一些消息:
class 返回對(duì)象的類;
isKindOfClass 和 isMemberOfClass檢查對(duì)象是否在指定的類繼承體系中;
respondsToSelector 檢查對(duì)象能否相應(yīng)指定的消息;
conformsToProtocol 檢查對(duì)象是否實(shí)現(xiàn)了指定協(xié)議類的方法;
methodForSelector 返回指定方法實(shí)現(xiàn)的地址。
performSelector:withObject 執(zhí)行SEL 所指代的方法。
消息轉(zhuǎn)發(fā):
通常,給一個(gè)對(duì)象發(fā)送它不能處理的消息會(huì)得到出錯(cuò)提示,然而,Objective-C運(yùn)行時(shí)系統(tǒng)在拋出錯(cuò)誤之前,會(huì)給消息接收對(duì)象發(fā)送一條特別的消息forwardInvocation 來(lái)通知該對(duì)象,該消息的唯一參數(shù)是個(gè)NSInvocation類型的對(duì)象——該對(duì)象封裝了原始的消息和消息的參數(shù)。
我們可以實(shí)現(xiàn)forwardInvocation:方法來(lái)對(duì)不能處理的消息做一些默認(rèn)的處理,也可以將消息轉(zhuǎn)發(fā)給其他對(duì)象來(lái)處理,而不拋出錯(cuò)誤。
關(guān)于消息轉(zhuǎn)發(fā)的作用,可以考慮如下情景:假設(shè),我們需要設(shè)計(jì)一個(gè)能夠響應(yīng)negotiate消息的對(duì)象,并且能夠包括其它類型的對(duì)象對(duì)消息的響應(yīng)。 通過(guò)在negotiate方法的實(shí)現(xiàn)中將negotiate消息轉(zhuǎn)發(fā)給其它的對(duì)象來(lái)很容易的達(dá)到這一目的。
更進(jìn)一步,假設(shè)我們希望我們的對(duì)象和另外一個(gè)類的對(duì)象對(duì)negotiate的消息的響應(yīng)完全一致。一種可能的方式就是讓我們的類繼承其它類的方法實(shí)現(xiàn)。 然后,有時(shí)候這種方式不可行,因?yàn)槲覀兊念惡推渌惪赡苄枰诓煌睦^承體系中響應(yīng)negotiate消息。
雖然我們的類無(wú)法繼承其它類的negotiate方法,但我們?nèi)匀豢梢蕴峁┮粋€(gè)方法實(shí)現(xiàn),這個(gè)方法實(shí)現(xiàn)只是簡(jiǎn)單的將negotiate消息轉(zhuǎn)發(fā)給其他類的對(duì)象,就好像從其它類那兒“借”來(lái)的現(xiàn)一樣。如下所示:
- negotiate { if ([someOtherObject respondsToSelector:@selector(negotiate)]) return [someOtherObject negotiate]; return self; } |
這種方式顯得有欠靈活,特別是有很多消息都希望傳遞給其它對(duì)象時(shí),我們就必須為每一種消息提供方法實(shí)現(xiàn)。此外,這種方式不能處理未知的消息。當(dāng)我們寫下代碼時(shí),所有我們需要轉(zhuǎn)發(fā)的消息的集合都必須確定。然而,實(shí)際上,這個(gè)集合會(huì)隨著運(yùn)行時(shí)事件的發(fā)生,新方法或者新類的定義而變化。
forwardInvocation:消息給這個(gè)問(wèn)題提供了一個(gè)更特別的,動(dòng)態(tài)的解決方案:當(dāng)一個(gè)對(duì)象由于沒(méi)有相應(yīng)的方法實(shí)現(xiàn)而無(wú)法響應(yīng)某消息時(shí),運(yùn)行時(shí)系統(tǒng)將通過(guò)forwardInvocation:消息通知該對(duì)象。每個(gè)對(duì)象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實(shí)現(xiàn)只是簡(jiǎn)單地調(diào)用了doesNotRecognizeSelector:。通過(guò)實(shí)現(xiàn)我們自己的forwardInvocation:方法,我們可以在該方法實(shí)現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對(duì)象。
要轉(zhuǎn)發(fā)消息給其它對(duì)象,forwardInvocation:方法所必須做的有:
1,決定將消息轉(zhuǎn)發(fā)給誰(shuí),并且
2,將消息和原來(lái)的參數(shù)一塊轉(zhuǎn)發(fā)出去。
消息可以通過(guò)invokeWithTarget:方法來(lái)轉(zhuǎn)發(fā):
- (void) forwardInvocation:(NSInvocation *)anInvocation { if ([someOtherObject respondsToSelector:[anInvocation selector]]) [anInvocation invokeWithTarget:someOtherObject]; else [super forwardInvocation:anInvocation]; } |
轉(zhuǎn)發(fā)消息后的返回值將返回給原來(lái)的消息發(fā)送者。您可以將返回任何類型的返回值,包括: id,結(jié)構(gòu)體,浮點(diǎn)數(shù)等。forwardInvocation:方法就像一個(gè)不能識(shí)別的消息的分發(fā)中心,將這些消息轉(zhuǎn)發(fā)給不同接收對(duì)象?;蛘咚部梢韵笠粋€(gè)運(yùn)輸站將所有的消息都發(fā)送給同一個(gè)接收對(duì)象。它可以將一個(gè)消息翻譯成另外一個(gè)消息,或者簡(jiǎn)單的"吃掉“某些消息,因此沒(méi)有響應(yīng)也沒(méi)有錯(cuò)誤。forwardInvocation:方法也可以對(duì)不同的消息提供同樣的響應(yīng),這一切都取決于方法的具體實(shí)現(xiàn)。該方法所提供是將不同的對(duì)象鏈接到消息鏈的能力。
注意: forwardInvocation:方法只有在消息接收對(duì)象中無(wú)法正常響應(yīng)消息時(shí)才會(huì)被調(diào)用。 所以,如果我們希望一個(gè)對(duì)象將negotiate消息轉(zhuǎn)發(fā)給其它對(duì)象,則這個(gè)對(duì)象不能有negotiate方法,也不能在動(dòng)態(tài)方法決議中為之提供實(shí)現(xiàn)。否則,forwardInvocation:將不可能會(huì)被調(diào)用。
參考資料:
Objective-CRuntime Reference:
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html
Objective-C Runtime Programming Guide:
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html