第二章 對象、消息、運行期—第12條:理解消息轉發(fā)機制

第11條講解了對象的消息傳遞機制,并強調了其重要性。第12條則要講解另外一個重要的問題,就是對象在收到無法解讀的消息之后會發(fā)生什么情況。
若想令類能理解某條消息,我們必須以程序碼實現(xiàn)出對應的方法才行。但是,在編譯期向類發(fā)送了無法解讀的消息并不會報錯,因為在運行期可以繼續(xù)向類中添加方法,所以編譯器在編譯時還無法確知類中到底會不會有某個方法實現(xiàn)。當對象接收到無法解讀的消息后,就會啟動"消息轉發(fā)"(message forwarding)機制,程序員可經由此過程告訴對象應該如何處理未知消息。
你可能早就遇到過經由消息轉發(fā)流程所處理的消息了,只是未加留意。如果在控制臺中看到下面這種提示信息,那就說明你曾向某個對象發(fā)送過一條其無法解讀的消息,從而啟動了消息轉發(fā)機制,并將此消息轉發(fā)給了NSObject的默認實現(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的選擇子。本例所列舉的這種情況并不奇怪,因為NSNumber類里本來就沒有名為lowercaseString的方法??刂婆_中看到的那個__NSFCNumber是為了實現(xiàn)"無縫橋接"(toll-free bridging, 第49條將會詳解此技術)而使用的內部類(internal class),配置NSNumber對象時也會一并創(chuàng)建此對象。在本例中,消息轉發(fā)過程以應用程序崩潰而告終,不過,開發(fā)者在編寫自己的類時,可于轉發(fā)過程中設置掛鉤,用以執(zhí)行預定的邏輯,而不使應用程序崩潰。
消息轉發(fā)分為兩大階段。第一階段先征詢接收者,所屬的類,看其是否能動態(tài)添加方法,以處理當前這個"未知的選擇子"(unknown selector),這叫做"動態(tài)方法解析"(dynamic method resolution)。第二階段涉及"完整的消息轉發(fā)機制"(full forwarding mechanism)。如果運行期系統(tǒng)已經把第一階段執(zhí)行完了,那么接收者自己就無法再以動態(tài)新增方法的手段來響應包含該選擇子的消息了。此時,運行期系統(tǒng)會請求接收者以其他手段來處理與消息相關的方法調用。這又細分為兩小步。首先,請接收者看看有沒有其他對象能處理這條消息。若有,則運行期系統(tǒng)會把消息轉給那個對象,于是消息轉發(fā)過程結束,一切如常。若沒有"背援的接收者"(replacement receiver),則啟動完整的消息轉發(fā)機制,運行期系統(tǒng)會把與消息有關的全部細節(jié)都封裝到NSInvocation對象中,再給接收者最后一次機會,令其設法解決當前還未處理的這條消息。

動態(tài)方法解析
對象在收到無法解讀的消息后,首先將調用其所屬類的下列類方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

該方法的參數(shù)就是那個未知的選擇子,其返回值為Boolean類型,表示這個類是否能新增一個實例方法用以處理此選擇子。在繼續(xù)往下執(zhí)行轉發(fā)機制之前,本類有機會新增一個處理此選擇子的方法。假如尚未實現(xiàn)的方法不是實例方法而是類方法,那么運行期系統(tǒng)就會調用另外一個方法,該方法與"resolveInstanceMethod:"類似,叫做"resolveClassMethod:"。
使用這種辦法的前提是:相關方法的實現(xiàn)代碼已經寫好,只等著運行的時候動態(tài)插在類里面就可以了。此方案常用來實現(xiàn)@dynamic屬性,比如說,要訪問CoreData框架中NSManagedObjects對象的屬性時就可以這么做,因為實現(xiàn)這些屬性所需的存取方法在編譯期就能確定。
下列代碼演示了如何用"resolveInstanceMethod:"來實現(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];
}

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

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

- (id)forwardingTargetForSelector:(SEL)selector

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

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

- (void)forwardInvocation:(NSIncation *)invocation

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

消息轉發(fā)全流程

消息轉發(fā)機制處理消息的各個步驟:


屏幕快照 2017-04-05 13.42.31.png

接收者在每一步中均有機會處理消息。步驟越往后,處理消息的代價就越大。最好能在第一步就處理完,這樣的話,運行期系統(tǒng)就可以將此方法緩存起來了。如果這個類的實例稍后還收到同名選擇子,那么根本無須啟動消息轉發(fā)流程。若想在第三步里把消息轉給備援的接收者,那還不如把轉發(fā)操作提前到第二步。因為第三步只是修改了調用目標,這項改動放在第二步執(zhí)行會更為簡單,不然的話,還得創(chuàng)建并處理完整的NSInvocation。

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

#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

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

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

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

@implementation EOCAutoDictionary

@dynamic string, number, date, opaqueObject;

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

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

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

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

id autoDictionaryGetter(id self, SEL _cmd) {
      //Get the backing store from object
      EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
      NSMutableDictionary *backingStore = typedSelf.backingStore;

      //The key is simply the sector 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 *typedStore = (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的用法很簡單:

EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@", dict.date);
//Output: dict.date = 1985-01-24 00:00:00 +0000

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

要點

  • 若對象無法響應某個選擇子,則進入消息轉發(fā)流程。
  • 通過運行期的動態(tài)方法解析功能,我們可以在需要用到某個方法時再將其加入類中。
  • 對象可以把其無法解讀的某些選擇子轉交給其他對象來處理
  • 經過上述兩步之后,如果還是沒辦法處理選擇子,那就啟動完整的消息轉發(fā)機制。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容