runtime:消息轉(zhuǎn)發(fā)機(jī)制

若想令類能理解某條消息,我們必須以程序碼實(shí)現(xiàn)出對(duì)應(yīng)的方法才行。但是,在編譯期向類發(fā)送了無法解讀的消息并不會(huì)報(bào)錯(cuò),因?yàn)樵谶\(yùn)行期可以繼續(xù)向類中添加方法,所以編譯器在編譯時(shí)還無法確知類中到底會(huì)不會(huì)有某個(gè)方法實(shí)現(xiàn)。當(dāng)對(duì)象接收到無法解讀的消息后,就會(huì)啟動(dòng)"消息轉(zhuǎn)發(fā)"(message forwarding)機(jī)制,程序員可經(jīng)由此過程告訴對(duì)象應(yīng)該如何處理未知消息。

你可能早就遇到過經(jīng)由消息轉(zhuǎn)發(fā)流程所處理的消息了,只是未加留意。如果在控制臺(tái)中看到下面這種提示信息,那就說明你曾向某個(gè)對(duì)象發(fā)送過一條其無法解讀的消息,從而啟動(dòng)了消息轉(zhuǎn)發(fā)機(jī)制,并將此消息轉(zhuǎn)發(fā)給了NSObject的默認(rèn)實(shí)現(xiàn)。

- [__NSCFNumber lowercaseString]: unrecognized selector to instance 0x87
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87'

上面這段異常信息是由NSObject的"doesNotRecognizeSelector:"方法所拋出的,此異常表明:消息接收者的類型是NSCFNumber,而該接收者無法理解名為lowercaseString的選擇子。本例所列舉的這種情況并不奇怪,因?yàn)镹SNumber類里本來就沒有名為lowercaseString的方法。控制臺(tái)中看到的那個(gè)NSFCNumber是為了實(shí)現(xiàn)"無縫橋接"(toll-free bridging),而使用的內(nèi)部類(internal class),配置NSNumber對(duì)象時(shí)也會(huì)一并創(chuàng)建此對(duì)象。在本例中,消息轉(zhuǎn)發(fā)過程以應(yīng)用程序崩潰而告終,不過,開發(fā)者在編寫自己的類時(shí),可于轉(zhuǎn)發(fā)過程中設(shè)置掛鉤,用以執(zhí)行預(yù)定的邏輯,而不使應(yīng)用程序崩潰。

消息轉(zhuǎn)發(fā)分為兩大階段。第一階段先征詢接收者,所屬的類,看其是否能動(dòng)態(tài)添加方法,以處理當(dāng)前這個(gè)"未知的選擇子"(unknown selector),這叫做"動(dòng)態(tài)方法解析"(dynamic method resolution)。第二階段涉及"完整的消息轉(zhuǎn)發(fā)機(jī)制"(full forwarding mechanism)。如果運(yùn)行期系統(tǒng)已經(jīng)把第一階段執(zhí)行完了,那么接收者自己就無法再以動(dòng)態(tài)新增方法的手段來響應(yīng)包含該選擇子的消息了。此時(shí),運(yùn)行期系統(tǒng)會(huì)請(qǐng)求接收者以其他手段來處理與消息相關(guān)的方法調(diào)用。這又細(xì)分為兩小步。首先,請(qǐng)接收者看看有沒有其他對(duì)象能處理這條消息。若有,則運(yùn)行期系統(tǒng)會(huì)把消息轉(zhuǎn)給那個(gè)對(duì)象,于是消息轉(zhuǎn)發(fā)過程結(jié)束,一切如常。若沒有"背援的接收者"(replacement receiver),則啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制,運(yùn)行期系統(tǒng)會(huì)把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對(duì)象中,再給接收者最后一次機(jī)會(huì),令其設(shè)法解決當(dāng)前還未處理的這條消息。

動(dòng)態(tài)方法解析

對(duì)象在收到無法解讀的消息后,首先將調(diào)用其所屬類的下列類方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

該方法的參數(shù)就是那個(gè)未知的選擇子,其返回值為Boolean類型,表示這個(gè)類是否能新增一個(gè)實(shí)例方法用以處理此選擇子。在繼續(xù)往下執(zhí)行轉(zhuǎn)發(fā)機(jī)制之前,本類有機(jī)會(huì)新增一個(gè)處理此選擇子的方法。假如尚未實(shí)現(xiàn)的方法不是實(shí)例方法而是類方法,那么運(yùn)行期系統(tǒng)就會(huì)調(diào)用另外一個(gè)方法,該方法與"resolveInstanceMethod:"類似,叫做"resolveClassMethod:"。

使用這種辦法的前提是:相關(guān)方法的實(shí)現(xiàn)代碼已經(jīng)寫好,只等著運(yùn)行的時(shí)候動(dòng)態(tài)插在類里面就可以了。此方案常用來實(shí)現(xiàn)@dynamic屬性,比如說,要訪問CoreData框架中NSManagedObjects對(duì)象的屬性時(shí)就可以這么做,因?yàn)閷?shí)現(xiàn)這些屬性所需的存取方法在編譯期就能確定。
下列代碼演示了如何用"resolveInstanceMethod:"來實(shí)現(xiàn)@dynamic屬性:

id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);

+ (BOOL)resolveInstanceMethod:(SEL)selector{
      NSString *selectorString = NSStringFormSelector(selector);
      if(/* selector is from a @dynamic property */) {
          if ([selectorString hasPrefix: @"set"]) {
              class_addMethod(self, selector, )(IMP)autoDictionarySetter, "v@:@");
          }else {
              class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
          }
          return YES;
      }
 return [super resolveInstanceMethod:selector];
}

首先將選擇子化為字符串,然后檢測(cè)其是否表示設(shè)置方法。若前綴為set,則表示設(shè)置方法,否則就是獲取方法。不管哪種情況,都會(huì)把處理該選擇子的方法加到類里面,所添加的方法是用純C函數(shù)實(shí)現(xiàn)的。C函數(shù)可能會(huì)用代碼來操作相關(guān)的數(shù)據(jù)結(jié)構(gòu),類之中的屬性數(shù)據(jù)就存放在那些數(shù)據(jù)結(jié)構(gòu)里面。以CoreData為例,這些存取方法也許要和后端數(shù)據(jù)庫(kù)通信以便獲取或更新相應(yīng)的值。

備援接收者

當(dāng)前接收者還有第二次機(jī)會(huì)能處理未知的選擇子,在這一步中,運(yùn)行期系統(tǒng)會(huì)問它:能不能把這條消息轉(zhuǎn)給其他接收者來處理。與該步驟對(duì)應(yīng)的處理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

方法參數(shù)代表未知的選擇子,若當(dāng)前接收者能找到備援對(duì)象,則將其返回,若找不到就返回nil。通過此方案,我們可以用"組合"(composition)來模擬出"多重繼承"(multipleinheritance)的某些特性。在一個(gè)對(duì)象內(nèi)部,可能還有一系列其他對(duì)象,該對(duì)象可經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對(duì)象返回,這樣的話,在外界看來,好像是該對(duì)象親自處理了這些消息似的。
請(qǐng)注意,我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息。若是想在發(fā)送給備援接收者之前先修改消息內(nèi)容,那就得通過完整的消息轉(zhuǎn)發(fā)機(jī)制來做了。

完整的消息轉(zhuǎn)發(fā)

如果轉(zhuǎn)發(fā)算法已經(jīng)來到這一步的話,那么唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了。
首先創(chuàng)建NSInvocation對(duì)象,把與尚未處理的那條消息有關(guān)的全部細(xì)節(jié)都封于其中。此對(duì)象包含選擇子、目標(biāo)(target)及參數(shù)。在觸發(fā)NSInvocation對(duì)象時(shí),"消息派發(fā)系統(tǒng)"(message-dispatch system)將親自出馬,把消息指派給目標(biāo)對(duì)象。
此步驟會(huì)調(diào)用下列方法來轉(zhuǎn)發(fā)消息:

- (void)forwardInvocation:(NSIncation *)invocation

這個(gè)方法可以實(shí)現(xiàn)得很簡(jiǎn)單:只需改變調(diào)用目標(biāo),使消息在新目標(biāo)上得以調(diào)用即可。然而這樣實(shí)現(xiàn)出來的方法與"備援接收者"方案所實(shí)現(xiàn)的方法等效,所以很少有人采用這么簡(jiǎn)單的實(shí)現(xiàn)方式。比較有用的實(shí)現(xiàn)方式為:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另外一個(gè)參數(shù),或是改換選擇子,等等。
實(shí)現(xiàn)此方法時(shí),若發(fā)現(xiàn)某調(diào)用操作不應(yīng)由本類處理,則需調(diào)用超類的同名方法。這樣的話,繼承體系中的每個(gè)類都有機(jī)會(huì)處理此調(diào)用請(qǐng)求,直至NSObject。如果最后調(diào)用了NSObject類的方法,那么該方法還會(huì)繼而調(diào)用"doesNotRecognizeSelector:"以拋出異常,此異常表明選擇子最終未能得到處理。

消息轉(zhuǎn)發(fā)全流程

消息轉(zhuǎn)發(fā)機(jī)制處理消息的各個(gè)步驟:

00353.jpg

接收者在每一步中均有機(jī)會(huì)處理消息。步驟越往后,處理消息的代價(jià)就越大。最好能在第一步就處理完,這樣的話,運(yùn)行期系統(tǒng)就可以將此方法緩存起來了。如果這個(gè)類的實(shí)例稍后還收到同名選擇子,那么根本無須啟動(dòng)消息轉(zhuǎn)發(fā)流程。若想在第三步里把消息轉(zhuǎn)給備援的接收者,那還不如把轉(zhuǎn)發(fā)操作提前到第二步。因?yàn)榈谌街皇切薷牧苏{(diào)用目標(biāo),這項(xiàng)改動(dòng)放在第二步執(zhí)行會(huì)更為簡(jiǎn)單,不然的話,還得創(chuàng)建并處理完整的NSInvocation。

以完整的例子演示動(dòng)態(tài)方法解析

為了說明消息轉(zhuǎn)發(fā)機(jī)制的意義,下面示范如何以動(dòng)態(tài)方法解析來實(shí)現(xiàn)@dynamic屬性。假設(shè)要編寫一個(gè)類似于"字典"的對(duì)象,它里面可以容納其他對(duì)象,只不過開發(fā)者要直接通過屬性來存取其中的數(shù)據(jù)。這個(gè)類的設(shè)計(jì)思路是:由開發(fā)者來添加屬性定義,并將其聲明為@dynamic,而類則會(huì)自動(dòng)處理相關(guān)屬性值得存放與獲取操作。
該類的接口可以寫成:

#import <Foundation/Foundation.h>

@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end

在類的內(nèi)部,每個(gè)屬性的值還是會(huì)存放在字典里,所以我們先在類中編寫如下代碼,并將屬性聲明為@dynamic,這樣的話,編譯器就不會(huì)為其自動(dòng)生成實(shí)例變量及存取方法了:

#import "EOCAutoDictionary.h"
#import <objc/runtime.h>

@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end

@implementation EOCAutoDictionary

@dynamic string, number, date, opaqueObject;

- (instancetype)init {
    self = [super init];
    if (self) {
        _backingStore = [NSMutableDictionary new];
    }
    return self;
}

本例的關(guān)鍵在于resolveInstanceMethod:方法的實(shí)現(xiàn)代碼:

id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
    } else {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}

當(dāng)開發(fā)者首次在EOCAutoDictionary實(shí)例上訪問某個(gè)屬性時(shí),運(yùn)行期系統(tǒng)還找不到對(duì)應(yīng)的選擇子,因?yàn)樗璧倪x擇子既沒有直接實(shí)現(xiàn),也沒有合成出來。現(xiàn)在假設(shè)要寫入opaqueObject屬性,那么系統(tǒng)就會(huì)以"setOpaqueObject:"為選擇子來調(diào)用上面這個(gè)方法。同理,在讀取該屬性時(shí),系統(tǒng)也會(huì)調(diào)用上述方法,只不過傳入的選擇子是opaqueObject。resolveInstanceMethod方法會(huì)判斷選擇子的前綴是否為set,以此分辨其是set選擇子還是get選擇子。在這兩種情況下,都要向類中新增一個(gè)處理該選擇子所用的方法,這兩個(gè)方法分別以autoDictionarySetter及autoDictionaryGetter函數(shù)指針的形式出現(xiàn)。此時(shí)就用到了class_addMethod方法,它可以向類中動(dòng)態(tài)地添加方法,用以處理給定的選擇子。第三個(gè)參數(shù)為函數(shù)指針,指向待添加的方法。而最后一個(gè)參數(shù)則表示待添加方法的"類型編碼"(type encoding)。在本例中,編碼開頭的字符表示方法的返回值類型,后續(xù)字符則表示其所接受的各個(gè)參數(shù)。
getter函數(shù)可以用下列代碼實(shí)現(xiàn):

id autoDictionaryGetter(id self, SEL _cmd) {
    // Get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    // The key is simply the selector name
    NSString *key = NSStringFromSelector(_cmd);
    
    // Return the value
    return [backingStore objectForKey:key];
}

而setter函數(shù)則可以這么寫:

void autoDictionarySetter(id self, SEL _cmd, id value) {
    // Get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    /** The selector will be for example, "setOpaqueObject:".
     *  We need to remove the "set", ":" and lowercase the first
     *  letter of the remainder.
     */
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    // Remove the ':' at the end
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    
    // Remove the 'set' prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    // Lowercase the first character
    NSString *lowercaseFirstChar =
    [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1)
                       withString:lowercaseFirstChar];
    
    if (value) {
        [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];
    }
}

EOCAutoDictionary的用法很簡(jiǎn)單:

EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@", dict.date);

其他屬性的訪問方式與date類似,要想添加新屬性,只需用@property來定義,并將其聲明為@dynamic即可。在iOS的CoreAnimation框架中,CALayer類就用了與本例相似的實(shí)現(xiàn)方式,這使得CALayer成為"兼容于鍵值編碼的"(key-value-coding-compliant)容器類,也就等于說,能夠向里面隨意添加屬性,然后以鍵值對(duì)的形式來訪問。于是,開發(fā)者就可以向其中新增自定義的屬性了,這些屬性值得存儲(chǔ)工作由基類直接負(fù)責(zé),我們只需在CALayer的子類中定義新屬性即可。

總結(jié)

1、若對(duì)象無法響應(yīng)某個(gè)選擇子,則進(jìn)入消息轉(zhuǎn)發(fā)流程。
2、通過運(yùn)行期的動(dòng)態(tài)方法解析功能,我們可以在需要用到某個(gè)方法時(shí)再將其加入類中。
3、對(duì)象可以把其無法解讀的某些選擇子轉(zhuǎn)交給其他對(duì)象來處理
4、經(jīng)過上述兩步之后,如果還是沒辦法處理選擇子,那就啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制。

摘自《Effective_Objective-C 2.0 編寫高質(zhì)量iOS和OS X代碼的52個(gè)有效方法記錄》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評(píng)論 6 545
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,795評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評(píng)論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,773評(píng)論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,106評(píng)論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,282評(píng)論 0 291
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,793評(píng)論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,507評(píng)論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,741評(píng)論 1 375
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,929評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評(píng)論 1 296
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,482評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,702評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容