深入淺出ObjC之消息 羅朝輝 (http://ww./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++ 中的:
在ObjC 中則如下:
Class 的含義 Class 被定義為一個(gè)指向 objc_class的結(jié)構(gòu)體指針,這個(gè)結(jié)構(gòu)體表示每一個(gè)類的類結(jié)構(gòu),。而 objc_class 在objc/objc_class.h中定義如下:
由此可見,,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中定義如下:
SEL 的含義: 在前面我們看到方法選標(biāo) SEL 的定義為: typedef struct objc_selector *SEL; 它是一個(gè)指向 objc_selector 指針,,表示方法的名字/簽名。如下所示,,打印出 selector,。
不同的類可以擁有相同的 selector,這個(gè)沒有問(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):
使用methodForSelector:來(lái)避免動(dòng)態(tà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è)大致思路了:示例
中對(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)找到的,。 上面的思路還有一些沒有提及的話題,,比如說(shuō)編譯器插入了什么代碼,如果在方法鏈表中沒有找到對(duì)應(yīng)的 IMP又會(huì)如何,,這些話題在下面展開,。
消息函數(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)綁定所需要的一切工作: 編譯器會(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)樗鼈儾]有在定義方法的源代碼中聲明,而是在代碼編譯時(shí)是插入方法的實(shí)現(xiàn)中的,。 盡管這些參數(shù)沒有被顯示聲明,,但在源代碼中仍然可以引用它們(就象可以引用消息接收者對(duì)象的實(shí)例變量一樣)。在方法中可以通過(guò)self來(lái)引用消息接收者對(duì)象,,通過(guò)選標(biāo)_cmd來(lái)引用方法本身,。在下面的例子中,_cmd 指的是strange方法,,self指的收到strange消息的對(duì)象,。
在這兩個(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; }
1,,首先去該類的方法 cache 中查找,如果找到了就返回它,; 2,,如果沒有找到,就去該類的方法列表中查找,。如果在該類的方法列表中找到了,,則將 IMP 返回,并將它加入cache中緩存起來(lái),。根據(jù)最近使用原則,,這個(gè)方法再次調(diào)用的可能性很大,緩存起來(lái)可以節(jié)省下次調(diào)用再次查找的開銷,。3,,3,如果在該類的方法列表中沒找到對(duì)應(yīng)的 IMP,,在通過(guò)該類結(jié)構(gòu)中的 super_class指針在其父類結(jié)構(gòu)的方法列表中去查找,,直到在某個(gè)父類的方法列表中找到對(duì)應(yīng)的IMP,返回它,,并加入cache中,; 4,如果在自身以及所有父類的方法列表中都沒有找到對(duì)應(yīng)的 IMP,,則看是不是可以進(jìn)行動(dòng)態(tài)方法決議(后面有專文講述這個(gè)話題),; 5,如果動(dòng)態(tà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ò)誤,。
更進(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)一樣,。如下所示:
這種方式顯得有欠靈活,,特別是有很多消息都希望傳遞給其它對(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ì)象由于沒有相應(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:方法所必須做的有: 消息可以通過(guò)invokeWithTarget:方法來(lái)轉(zhuǎn)發(fā):
轉(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)單的"吃掉“某些消息,因此沒有響應(yīng)也沒有錯(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: Objective-C Runtime Programming Guide:http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html |
|