若想令類能夠理解某條消息,我們必須實現(xiàn)出對應的方法才行鲸湃。但是子寓,在編譯器向類發(fā)送其無法解讀的消息時并不會報錯,因為在運行期可以繼續(xù)向類中添加方法炸裆,所以編譯器在編譯時還無法確定類中到底會不會有某個方法的實現(xiàn)鲜屏。當對象接收到無法解讀的消息時,就會啟動“消息轉(zhuǎn)發(fā)”機制惯殊,我們可以經(jīng)由此過程告訴對象應該如何處理未知消息也殖。
可能遇到的經(jīng)由消息轉(zhuǎn)發(fā)機制所處理的消息,控制臺出現(xiàn)下面這種提示信息己儒,那就說明你曾向某個對象發(fā)送過一條其無法解讀的消息捆毫,就會啟動消息轉(zhuǎn)發(fā)機制绩卤,并將此消息轉(zhuǎn)發(fā)給了NSObject的默認實現(xiàn)
-[__NSCFNumber lowercaseString] :unrecognized selector send to instance 0x87
消息轉(zhuǎn)發(fā)分為兩個階段江醇。第一階段先征詢接收者所屬的類何暇,看其是否能動態(tài)添加方法,已處理當前這個“未知的選擇子”,這叫做“動態(tài)方法解析”遏插。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機制”纠修。如果運行期系統(tǒng)已經(jīng)把第一階段執(zhí)行完了,那么接收者自己就無法再以動態(tài)新增方法的手段來響應包含該選擇子的消息了了牛。此時運行期系統(tǒng)就會請求接收者以其他手段來處理與消息相關(guān)的方法調(diào)用辰妙。細分為兩步:首先,讓接收者看看有沒有其他對象能處理這條消息蛙婴。如果有尔破,則運行期系統(tǒng)會把消息轉(zhuǎn)給那個接收者懒构,于是消息轉(zhuǎn)發(fā)結(jié)束。如果沒有這個“備援接收者”胆剧,則啟動完整的消息轉(zhuǎn)發(fā)機制秩霍,運行期系統(tǒng)會把與消息有關(guān)的全部細節(jié)封裝到NSInvocation對象中,再給接收者最后一次機會辕近,令其設(shè)法解決當前還未處理的這條消息匿垄。
動態(tài)方法解析
對象在收到無法解讀的消息后归粉,首先將調(diào)用其所屬類的下列類方法:
+(BOOL)resolveInstanceMethod:(SEL)sel ;
sel 就是那個未知的選擇子糠悼,該方法返回值為BOOL類型浅乔,表示這個類是否能增一個實例方法來處理此選擇子。在繼續(xù)往下執(zhí)行轉(zhuǎn)發(fā)機制之前席噩,本類有機會新增一個處理此選擇子的方法贤壁。
假如尚未實現(xiàn)的方法是類方法,則調(diào)用
+(BOOL)resolveClassMethod:(SEL)sel ;
此方案常用來實現(xiàn)@dynamic屬性馒索,文章后面會寫出完整例子
備援接收者
當前接收者還有第二次機會處理未知的選擇子名船,在這一步中渠驼,運行期系統(tǒng)會詢問是否能將該消息轉(zhuǎn)發(fā)給其他的接收者處理。
- (id)forwardingTargetForSelector:(SEL)aSelector
方法參數(shù)代表未知的選擇子疯趟,若當前接收者能找到備援對象谋梭,則將其返回,找不到就返回nil盹舞。通過此方案隘庄,我們可以用“組合”來模擬出“多重繼承”的某些特性。在一個對象內(nèi)部获印,可能還有一系列其他對象街州,該對象可以經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對象返回,這樣的話鳍征, 在外界看來好像是該對象親自處理了這些消息艳丛。
請注意,我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息碰酝,若是想在發(fā)送給備援接收者之前先修改消息內(nèi)容眶蕉,那就得通過完整的消息轉(zhuǎn)發(fā)機制來做了唧躲。
完整的消息轉(zhuǎn)發(fā)
如果轉(zhuǎn)發(fā)已經(jīng)到了這一步,那么唯一能做的就是啟動完整的消息轉(zhuǎn)發(fā)機制了饭入。首先創(chuàng)建NSIvocation對象肛真,把尚未處理的那條消息有關(guān)的細節(jié)全部封到其中蚓让。此對象包含選擇子、目標窄瘟、參數(shù)趟卸。在出發(fā)NSIvocation對象時,”消息派發(fā)系統(tǒng)“將親自出馬图云,把消息指派給目標對象邻邮。
此步驟會調(diào)用下列方法來轉(zhuǎn)發(fā)消息:
-(void)forwardInvocation:(NSInvocation *)anInvocation
這個方法的實現(xiàn)可以寫的很簡單筒严,只需要改變調(diào)用目標鸠补,使消息在新目標上得以調(diào)用即可嘀掸。然而這樣實現(xiàn)出來的方法與”備援接收者“反感所實現(xiàn)的方法等效睬塌,所以很少有人采用這么簡單的實現(xiàn)方式。比較有用的實現(xiàn)方式為:在觸發(fā)消息前勋陪,先以某種方式改變消息內(nèi)容硫兰,比如追加另外一個參數(shù),或者是改換選擇子等等违孝。實現(xiàn)此方法時若發(fā)現(xiàn)不應該由本類處理泳赋,則需要調(diào)用超類的同名方法祖今。這樣的話,集成體系中的每個類都有機會處理此調(diào)用請求耍目,直至NSObject徐绑。如果最后調(diào)用了NSObject類的方法,那么該方法還會繼而調(diào)用”doesNotRecognizeSelector:“以拋出異常耕捞,此異常表明選擇子最終未能得到處理俺抽。
消息轉(zhuǎn)發(fā)全流程
接收者在每一步中均有機會處理消息较曼。步驟越往后,處理消息的代價就會越大弛饭。最好能在第一步就完成侣颂,這樣的話,運行期系統(tǒng)就可以將此方法緩存起來藻肄。如果這個類的實例后面還收到同名的選擇子拒担,那么根本就無須啟動消息轉(zhuǎn)發(fā)流程。若想在第三部把消息轉(zhuǎn)發(fā)給備援接收者州弟,還不如把轉(zhuǎn)發(fā)操作提前到第二部婆翔。以為第三部只是修改了調(diào)用目標毁兆,這項改動放在第二部執(zhí)行的話會更加簡單阴挣,不然還得創(chuàng)建并處理完整的NSIvocation。
完整例子演示動態(tài)方法解析
為了說明消息轉(zhuǎn)發(fā)機制的意義茎芭,下面示范如何以動態(tài)方法解析來實現(xiàn)@dynamic屬性梅桩。假設(shè)要編寫一個類似于”字典的對象“拜隧,它里面可以容納其他對象,只不過開發(fā)者要直接通過屬性來存取其中的數(shù)據(jù)垦页。這個類的設(shè)計思路是:由開發(fā)者來添加數(shù)據(jù)定義痊焊,并將其聲明為@dynamic,而類則會自動處理相關(guān)屬性值得存放與獲取操作薄啥。
新建一個繼承于NSObject的類EOCAutoDictionary
#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
在類EOCAutoDictionary的內(nèi)部垄惧,每個屬性的值還會存放在字典里到逊,所以在類中編寫如下代碼,并將屬性聲明為@dynamic枷踏,這樣的話編譯器就不會為其自動生成實例變量和存取方法了
#import "EOCAutoDictionary.h"
#import <objc/runtime.h>
@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end
@implementation EOCAutoDictionary
@dynamic string,number,date,opaqueObject; // 通過dynamic修飾之后編譯器不會為其自動生成實例變量及存取方法
- (id)init {
if (self = [super init]) {
_backingStore = [NSMutableDictionary new];
}
return self;
}
當對象收到無法解讀的消息后掰曾,調(diào)用下面所屬類方法
// 處理實例方法
+(BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString hasPrefix:@"set"]) {
/** class_addMethod函數(shù)解讀如下:
class_addMethod(Class cls, SEL name,IMP imp,const char *types)
1旷坦、 Class cls:這里需要一個類 [XXX class] 在這里用self,相當于[EOCAutoDictionary class]
2、 SEL name: 這里的方法命名隨意旗芬,就是添加的方法在本類里叫做的名字捆蜀,但是方法的格式要和你添加的方法的格式一樣辆它,比如有無參數(shù), 這里直接拿sel即可呢蔫,相當于幾個屬性的setter和getter方法名
3飒筑、 IMP imp:函數(shù)指針协屡,指向添加的方法,
需要實現(xiàn)下面這個方法联予,也就是runtime的方法,獲取對應的函數(shù)的指針季眷,也就是IMP
// OBJC_EXPORT IMP 函數(shù)返回值是 IMP指針
// class_getMethodImplementation 函數(shù)名
括號里各個參數(shù)意義:
(1)Class cls 一個class
(2)SEL name 方法名
OBJC_EXPORT IMP class_getMethodImplementation(Class cls, SEL name)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
4子刮、 const char *types: 'v@:' ==> void methodName(Class cls, SEL name){}
'i@:' ==> int methodName(Class cls, SEL name){}
'i@:@' ==> int methodName(Class cls, SEL name,parameter){}
'@@:' ==> id methodName(Class cls, SEL name){}
以:為界窑睁,左邊代表函數(shù)返回值類型,右邊表示參數(shù)個數(shù)和類型
*/
// 向類中動態(tài)的添加方法
class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
}else {
class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
通過函數(shù)class_addMethod動態(tài)添加的函數(shù)如下:
getter函數(shù)如下:
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 seletor will be for example "setOpaqueObject:".
We need to remove the "set",":",and lowercase the first letter of the remiander
方法類似 "setOpaqueObject:" 我們需要刪除前面的 'set' 和后面的 ':' 并且讓第一個字母小寫
*/
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的用法如下
#import "ViewController.h"
#import "EOCAutoDictionary.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
EOCAutoDictionary *dict = [[EOCAutoDictionary alloc] init];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
dict.string = @"編寫iOS OS X代碼的52個高效有效方法";
NSLog(@"dict.date = %@",dict.date);
NSLog(@"dict.string = %@",dict.string);
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
要點:
1箫津、若對象無法響應某個選擇子苏遥,則進入消息轉(zhuǎn)發(fā)流程
2、通過運行期的動態(tài)方法解析功能师抄,我們可以在需要用到某個方法時再將其加入類中
3教硫、對象可以把其無法解讀的某些選擇子轉(zhuǎn)交給其他對象來處理
4、經(jīng)過上述幾步之后挤安,如果還是無法處理選擇子丧鸯,那就啟動完整的消息轉(zhuǎn)發(fā)機制