深刻淺出Cocoa之消息 html
羅朝輝 (http://www.cnblogs.com/kesalin/)程序員
在入門級別的ObjC 教程中,咱們常對從C++或Java 或其餘面嚮對象語言轉過來的程序員說,ObjC 中的方法調用(ObjC中的術語爲消息)跟其餘語言中的方法調用差很少,只是形式有些不一樣而已。 緩存
譬如C++ 中的:app
Bird * aBird = new Bird();ide
aBird->fly();函數
在ObjC 中則以下:post
Bird * aBird = [[Bird alloc] init];ui
[aBird fly];url
初看起來,好像只是書寫形式不一樣而已,實則差別大矣。C++中的方法調用多是動態的,也多是靜態的;而ObjC中的消息都爲動態的。下文將詳細介紹爲何是動態的,以及編譯器在這背後作了些什麼事情。翻譯
要說清楚消息這個話題,咱們必須先來了解三個概念Class, SEL, IMP,它們在objc/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 被定義爲一個指向 objc_class的結構體指針,這個結構體表示每個類的類結構。而 objc_class 在objc/objc_class.h中定義以下:
struct objc_class {
struct objc_class * isa;
struct objc_class * super_class; /*父類*/
const char *name; /*類名字*/
long version; /*版本信息*/
long info; /*類信息*/
long instance_size; /*實例大小*/
struct objc_ivar_list *ivars; /*實例參數鏈表*/
struct objc_method_list **methodLists; /*方法鏈表*/
struct objc_cache *cache; /*方法緩存*/
struct objc_protocol_list *protocols; /*協議鏈表*/
};
因而可知,Class 是指向類結構體的指針,該類結構體含有一個指向其父類類結構的指針,該類方法的鏈表,該類方法的緩存以及其餘必要信息。
NSObject 的class 方法就返回這樣一個指向其類結構的指針。每個類實例對象的第一個實例變量是一個指向該對象的類結構的指針,叫作isa。經過該指針,對象能夠訪問它對應的類以及相應的父類。如圖一所示:
如圖一所示,圓形所表明的實例對象的第一個實例變量爲 isa,它指向該類的類結構 The object’s class。而該類結構有一個指向其父類類結構的指針superclass, 以及自身消息名稱(selector)/實現地址(address)的方法鏈表。
方法的含義:
注意這裏所說的方法鏈表裏面存儲的是Method 類型的。圖一中selector 就是指 Method的 SEL, address就是指Method的 IMP。 Method 在頭文件 objc_class.h中定義以下:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;
char *method_types;
IMP method_imp;
};
一個方法 Method,其包含一個方法選標 SEL – 表示該方法的名稱,一個types – 表示該方法參數的類型,一個 IMP - 指向該方法的具體實現的函數指針。
SEL 的含義:
在前面咱們看到方法選標 SEL 的定義爲:
typedef struct objc_selector *SEL;
它是一個指向 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,這個沒有問題,由於不一樣類的實例對象performSelector相同的 selector 時,會在各自的消息選標(selector)/實現地址(address) 方法鏈表中根據 selector 去查找具體的方法實現IMP, 而後用這個方法實現去執行具體的實現代碼。這是一個動態綁定的過程,在編譯的時候,咱們不知道最終會執行哪一些代碼,只有在執行的時候,經過selector去查詢,咱們才能肯定具體的執行代碼。
IMP 的含義:
在前面咱們也看到 IMP 的定義爲:
typedef id (*IMP)(id, SEL, ...);
根據前面id 的定義,咱們知道 id是一個指向 objc_object 結構體的指針,該結構體只有一個成員isa,因此任何繼承自 NSObject 的類對象均可以用id 來指代,由於 NSObject 的第一個成員實例就是isa。
至此,咱們就很清楚地知道 IMP 的含義:IMP 是一個函數指針,這個被指向的函數包含一個接收消息的對象id(self 指針), 調用方法的選標 SEL (方法名),以及不定個數的方法參數,並返回一個id。也就是說 IMP 是消息最終調用的執行代碼,是方法真正的實現代碼 。咱們能夠像在C語言裏面同樣使用這個函數指針。
NSObject 類中的methodForSelector:方法就是這樣一個獲取指向方法實現IMP 的指針,methodForSelector:返回的指針和賦值的變量類型必須徹底一致,包括方法的參數類型和返回值類型。
下面的例子展現了怎麼使用指針來調用setFilled:的方法實現:
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:來避免動態綁定將減小大部分消息的開銷,可是這隻有在指定的消息被重複發送不少次時纔有意義,例如上面的for循環。
注意,methodForSelector:是Cocoa運行時系統的提供的功能,而不是Objective-C語言自己的功能。
消息調用過程:
至此咱們對ObjC 中的消息應該有個大體思路了:示例
Bird * aBird = [[Bird alloc] init];
[aBird fly];
中對 fly 的調用,編譯器經過插入一些代碼,將之轉換爲對方法具體實現IMP的調用,這個 IMP是經過在 Bird 的類結構中的方法鏈表中查找名稱爲fly 的 選標SEL 對應的具體方法實現找到的。
上面的思路還有一些沒有說起的話題,好比說編譯器插入了什麼代碼,若是在方法鏈表中沒有找到對應的 IMP又會如何,這些話題在下面展開。
消息函數 obj_msgSend:
編譯器會將消息轉換爲對消息函數 objc_msgSend的調用,該函數有兩個主要的參數:消息接收者id 和消息對應的方法選標 SEL, 同時接收消息中的任意參數:
id objc_msgSend(id theReceiver, SELtheSelector, ...)
如上面的消息 [aBird fly]會被轉換爲以下形式的函數調用:
objc_msgSend(aBird, @selector(fly));
該消息函數作了動態綁定所須要的一切工做:
1,它首先找到 SEL 對應的方法實現 IMP。由於不一樣的類對同一方法可能會有不一樣的實現,因此找到的方法實現依賴於消息接收者的類型。
2, 而後將消息接收者對象(指向消息接收者對象的指針)以及方法中指定的參數傳遞給方法實現 IMP。
3, 最後,將方法實現的返回值做爲該函數的返回值返回。
編譯器會自動插入調用該消息函數objc_msgSend的代碼,咱們無須在代碼中顯示調用該消息函數。當objc_msgSend找到方法對應的實現時,它將直接調用該方法實現,並將消息中全部的參數都傳遞給方法實現,同時,它還將傳遞兩個隱藏的參數:消息的接收者以及方法名稱 SEL。這些參數幫助方法實現得到了消息表達式的信息。它們被認爲是」隱藏「的是由於它們並無在定義方法的源代碼中聲明,而是在代碼編譯時是插入方法的實現中的。
儘管這些參數沒有被顯示聲明,但在源代碼中仍然能夠引用它們(就象能夠引用消息接收者對象的實例變量同樣)。在方法中能夠經過self來引用消息接收者對象,經過選標_cmd來引用方法自己。在下面的例子中,_cmd 指的是strange方法,self指的收到strange消息的對象。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if (target == self || mothod == _cmd)
return nil;
return [target performSelector:method];
}
在這兩個參數中,self更有用一些。實際上,它是在方法實現中訪問消息接收者對象的實例變量的途徑。
查找 IMP 的過程:
前面說了,objc_msgSend 會根據方法選標 SEL 在類結構的方法列表中查找方法實現IMP。這裏頭有一些文章,咱們在前面的類結構中也看到有一個叫objc_cache *cache 的成員,這個緩存爲提升效率而存在的。每一個類都有一個獨立的緩存,同時包括繼承的方法和在該類中定義的方法。。
查找IMP 時:
1,首先去該類的方法 cache 中查找,若是找到了就返回它;
2,若是沒有找到,就去該類的方法列表中查找。若是在該類的方法列表中找到了,則將 IMP 返回,並將它加入cache中緩存起來。根據最近使用原則,這個方法再次調用的可能性很大,緩存起來能夠節省下次調用再次查找的開銷。3,3,若是在該類的方法列表中沒找到對應的 IMP,在經過該類結構中的 super_class指針在其父類結構的方法列表中去查找,直到在某個父類的方法列表中找到對應的IMP,返回它,並加入cache中。
4,若是在自身以及全部父類的方法列表中都沒有找到對應的 IMP,則進入下文中要講的消息轉發流程。
便利函數:
咱們能夠經過NSObject的一些方法獲取運行時信息或動態執行一些消息:
class 返回對象的類;
isKindOfClass 和 isMemberOfClass檢查對象是否在指定的類繼承體系中;
respondsToSelector 檢查對象可否相應指定的消息;
conformsToProtocol 檢查對象是否實現了指定協議類的方法;
methodForSelector 返回指定方法實現的地址。
performSelector:withObject 執行SEL 所指代的方法。
消息轉發:
一般,給一個對象發送它不能處理的消息會獲得出錯提示,然而,Objective-C運行時系統在拋出錯誤以前,會給消息接收對象發送一條特別的消息forwardInvocation 來通知該對象,該消息的惟一參數是個NSInvocation類型的對象——該對象封裝了原始的消息和消息的參數。咱們能夠實現forwardInvocation:方法來對不能處理的消息作一些默認的處理,也能夠將消息轉發給其餘對象來處理,而不拋出錯誤。
關於消息轉發的做用,能夠考慮以下情景:假設,咱們須要設計一個可以響應negotiate消息的對象,而且可以包括其它類型的對象對消息的響應。 經過在negotiate方法的實現中將negotiate消息轉發給其它的對象來很容易的達到這一目的。
更進一步,假設咱們但願咱們的對象和另一個類的對象對negotiate的消息的響應徹底一致。一種可能的方式就是讓咱們的類繼承其它類的方法實現。 而後,有時候這種方式不可行,由於咱們的類和其它類可能須要在不一樣的繼承體系中響應negotiate消息。
雖然咱們的類沒法繼承其它類的negotiate方法,但咱們仍然能夠提供一個方法實現,這個方法實現只是簡單的將negotiate消息轉發給其餘類的對象,就好像從其它類那兒「借」來的現同樣。以下所示:
- negotiate
{
if ([someOtherObject respondsToSelector:@selector(negotiate)])
return [someOtherObject negotiate];
return self;
}
這種方式顯得有欠靈活,特別是有不少消息都但願傳遞給其它對象時,咱們就必須爲每一種消息提供方法實現。此外,這種方式不能處理未知的消息。當咱們寫下代碼時,全部咱們須要轉發的消息的集合都必須肯定。然而,實際上,這個集合會隨着運行時事件的發生,新方法或者新類的定義而變化。
forwardInvocation:消息給這個問題提供了一個更特別的,動態的解決方案:當一個對象因爲沒有相應的方法實現而沒法響應某消息時,運行時系統將經過forwardInvocation:消息通知該對象。每一個對象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現只是簡單地調用了doesNotRecognizeSelector:。經過實現咱們本身的forwardInvocation:方法,咱們能夠在該方法實現中將消息轉發給其它對象。
要轉發消息給其它對象,forwardInvocation:方法所必須作的有:
1,決定將消息轉發給誰,而且
2,將消息和原來的參數一塊轉發出去。
消息能夠經過invokeWithTarget:方法來轉發:
- (void) forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
轉發消息後的返回值將返回給原來的消息發送者。您能夠將返回任何類型的返回值,包括: id,結構體,浮點數等。
forwardInvocation:方法就像一個不能識別的消息的分發中心,將這些消息轉發給不一樣接收對象。或者它也能夠象一個運輸站將全部的消息都發送給同一個接收對象。它能夠將一個消息翻譯成另一個消息,或者簡單的"吃掉「某些消息,所以沒有響應也沒有錯誤。forwardInvocation:方法也能夠對不一樣的消息提供一樣的響應,這一切都取決於方法的具體實現。該方法所提供是將不一樣的對象連接到消息鏈的能力。
注意: forwardInvocation:方法只有在消息接收對象中沒法正常響應消息時纔會被調用。 因此,若是咱們但願一個對象將negotiate消息轉發給其它對象,則這個對象不能有negotiate方法。不然,forwardInvocation:將不可能會被調用。
消息轉發示例:
// Proxy @interface Proxy : NSObject -(void)MissMethod; @end @implementation Proxy -(void)MissMethod { NSLog(@" >> MissMethod() in Proxy."); } @end // Foo @interface Foo : NSObject @end @implementation Foo - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL name = [anInvocation selector]; NSLog(@" >> forwardInvocation for selector [%@]", NSStringFromSelector(name)); Proxy * proxy = [[[Proxy alloc] init] autorelease]; if ([proxy respondsToSelector:name]) { [anInvocation invokeWithTarget:proxy]; } else { [super forwardInvocation:anInvocation]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [Proxy instanceMethodSignatureForSelector:aSelector]; } @end // 調用代碼 Foo * foo = [[[Foo alloc] init] autorelease]; [foo MissMethod];
運行上面調用代碼將會輸出:
>> forwardInvocation MissMethod
>> MissMethod() in Proxy.
參考資料:
Objective-CRuntime Reference:
Objective-C Runtime Programming Guide:
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html