若想令類能理解某條消息,我們必須以程序碼實(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è)步驟:
接收者在每一步中均有機(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è)有效方法記錄》