前言:最近一直在研讀《Effective Objective-C 2.0》這篇文章弃理,覺得受益匪淺。自己將書上的代碼進(jìn)行實(shí)現(xiàn)费薄,再結(jié)合作者的解釋進(jìn)行思路擴(kuò)展之后那先,對(duì)oc語言機(jī)制有了更深的理解坷檩;
這篇文章結(jié)合了自身的理解却音,盡量更清楚明白的闡述消息轉(zhuǎn)發(fā)機(jī)制的實(shí)現(xiàn)過程和原理改抡,如有闡述不當(dāng)?shù)牡胤剑瑲g迎指導(dǎo)改正系瓢。
什么是消息轉(zhuǎn)發(fā)阿纤?
oc中方法調(diào)用就是一個(gè)消息傳遞的過程。例如:
[self testMethodWithInfo:info];
本例中夷陋,self被稱為“接收者”欠拾,testMethodWithInfo:被稱為選擇器,info為參數(shù)骗绕,選擇器和參數(shù)合起來稱為“消息”藐窄。
但是,在編譯期像對(duì)象發(fā)送了其無法解讀的消息之后酬土,編譯器并不會(huì)報(bào)錯(cuò),只會(huì)給出一個(gè)方法名未知的警告:
這是因?yàn)樵谶\(yùn)行期可以繼續(xù)向類中添加方法枷邪,所以編譯器在編譯時(shí)還無法確定類中到底會(huì)不會(huì)有某個(gè)方法實(shí)現(xiàn)。當(dāng)對(duì)象接收到無法解讀的消息后诺凡,就會(huì)啟動(dòng)“消息轉(zhuǎn)發(fā)”機(jī)制,我們也可以經(jīng)由此過程老告訴對(duì)象應(yīng)該如何處理未知消息践惑。
消息轉(zhuǎn)發(fā)機(jī)制的實(shí)現(xiàn)過程:
消息轉(zhuǎn)發(fā)分為兩大階段腹泌。第一個(gè)階段先征詢接收者所屬的類,看其是否能動(dòng)態(tài)添加方法尔觉,以處理當(dāng)前這個(gè)“未知的選擇器”凉袱,這叫做“動(dòng)態(tài)方法解析”。第二個(gè)階段涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”侦铜。如果運(yùn)行期征詢結(jié)果接收者沒能動(dòng)態(tài)添加方法以響應(yīng)包含該選擇器的消息专甩,此時(shí)系統(tǒng)會(huì)請(qǐng)求接收者以其他手段來處理與消息相關(guān)的方法調(diào)用。首先钉稍,請(qǐng)接收者看看有沒有其他對(duì)象能處理這條消息涤躲,如果有,系統(tǒng)會(huì)把消息轉(zhuǎn)發(fā)給那個(gè)對(duì)象贡未,消息轉(zhuǎn)發(fā)過程結(jié)束种樱。如果沒有“備援的接受者”,則啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制俊卤,系統(tǒng)會(huì)把與消息相關(guān)的全部細(xì)節(jié)都封裝在NSInvocation對(duì)象中嫩挤,再給接收者最后一次機(jī)會(huì),令其設(shè)法解決當(dāng)前還未處理的這條消息消恍。
動(dòng)態(tài)方法解析:
對(duì)象在收到無法解讀的消息后岂昭,首先會(huì)調(diào)用所屬類的下列類方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
參數(shù)sel就是未知的選擇器,返回值BOOL類型狠怨,表示這個(gè)類能否新增一個(gè)實(shí)例方法以處理此選擇器约啊。假如將要實(shí)現(xiàn)的不是實(shí)例方法而是類方法邑遏,系統(tǒng)則會(huì)調(diào)用另外一個(gè)方法:
+ (BOOL)resolveClassMethod:(SEL)sel
使用這種辦法的前提是:相關(guān)方法的實(shí)現(xiàn)代碼已經(jīng)寫好(否則識(shí)別不到新添加的方法名),只等著運(yùn)行的時(shí)候動(dòng)態(tài)插入到類里面就可以了棍苹。
演示代碼:
先實(shí)現(xiàn)替代方法:
#import "StudentModel.h"
void nullMethodSubstite(id self, SEL _cmd){
NSLog(@"message has transpond");
};
然后在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中進(jìn)行處理:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *str = NSStringFromSelector(sel);
if ([str containsString:@"nullMethod"]) {
/**
通過給定名稱向類中添加新方法
@param self 指定類
@param sel 待處理的選擇器
@param IMP 方法名
@return 用來描述方法參數(shù)類型的字符集
*/
class_addMethod(self, sel, (IMP)nullMethodSubstite, "@@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
1无宿、獲取選擇器;2枢里、檢測(cè)選擇器是否表示nullMethod方法孽鸡;3、向類中添加該方法
查看運(yùn)行結(jié)果:
備援接收者:
當(dāng)前接收者還有第二次機(jī)會(huì)能處理未知的選擇器栏豺,在該步驟中彬碱,系統(tǒng)會(huì)征詢接收者能不能把這條消息轉(zhuǎn)給其他接收者來處理。對(duì)應(yīng)的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
實(shí)現(xiàn)代碼:
@interface ModelPrintViewController ()
{
//新建Student類
StudentModel *_model;
}
@end
- (id)forwardingTargetForSelector:(SEL)aSelector{
if ([NSStringFromSelector(aSelector) containsString:@"nullMethod"]) {
return _model;
}
return [super forwardingTargetForSelector:aSelector];
}
在student類中實(shí)現(xiàn)代碼:
- (void)nullMethod{
NSLog(@"This is Student nullMethod");
}
1奥洼、判斷是否為nullMethod方法巷疼;2、設(shè)置備援接收者灵奖;3嚼沿、在備援接收者中實(shí)現(xiàn)該代碼
查看運(yùn)行結(jié)果:
注:我們無法操作經(jīng)由這一步轉(zhuǎn)發(fā)的消息,只能設(shè)置備援接收者替代當(dāng)前對(duì)象來接收這一消息瓷患,如果想在發(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)將親自出馬,把消息指派給目標(biāo)對(duì)象”洞渔。
此步驟會(huì)調(diào)用下列方法來轉(zhuǎn)發(fā)消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation
這個(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)此方法师郑,若發(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è)步驟洁闰。
以完整的例子演示動(dòng)態(tài)方法解析:
@dynamic: 使用@dynamic關(guān)鍵字聲明屬性,可以讓編譯器默認(rèn)不去自動(dòng)創(chuàng)建實(shí)現(xiàn)屬性所用的實(shí)例變量万细,也不會(huì)為其創(chuàng)建存取方法扑眉,并且編譯器不會(huì)報(bào)錯(cuò)。
為了說明消息轉(zhuǎn)發(fā)機(jī)制的意義赖钞,下面示范如何以動(dòng)態(tài)方法解析來實(shí)現(xiàn)@dynamic屬性腰素。
創(chuàng)建student對(duì)象,并且將其屬性用@dynamic關(guān)鍵字進(jìn)行聲明后雪营,在不創(chuàng)建實(shí)例變量和存取方法的情況下弓千,通過消息轉(zhuǎn)發(fā)機(jī)制實(shí)現(xiàn)其存取方法。示例如下:
#import <Foundation/Foundation.h>
@interface StudentModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *country;
@end
#import "StudentModel.h"
@interface StudentModel ()
//muDict用來傳值
@property (nonatomic, strong) NSMutableDictionary *muDict;
@end
@implementation StudentModel
@dynamic name```,city,country;
進(jìn)行此操作后卓缰,編譯器不會(huì)自動(dòng)創(chuàng)建實(shí)例變量和存取方法,此時(shí)用點(diǎn)語法進(jìn)行屬性的存取值時(shí)砰诵,運(yùn)行期系統(tǒng)找不到對(duì)應(yīng)的選擇器征唬,此時(shí)消息轉(zhuǎn)發(fā)機(jī)制啟動(dòng),系統(tǒng)會(huì)調(diào)用所屬類的+ (BOOL)resolveInstanceMethod:(SEL)sel方法茁彭,我們可以在此方法中對(duì)選擇器進(jìn)行處理:
首先實(shí)現(xiàn)替代方法:
id autoStudentGetter(id self, SEL _cmd){
StudentModel *model = (StudentModel *)self;
NSMutableDictionary *dict = model.muDict;
NSString *key = NSStringFromSelector(_cmd);
NSString *str = [[key substringFromIndex:3]lowercaseString];
return [dict objectForKey:str];
}
void autoStudentSetter(id self, SEL _cmd, id value){
StudentModel *model = (StudentModel *)self;
NSMutableDictionary *dict = model.muDict;
NSString *str = NSStringFromSelector(_cmd);
NSMutableString *key = [str mutableCopy];
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
[key deleteCharactersInRange:NSMakeRange(0, 3)];
NSString *lowerCharStr = [key lowercaseString];
if (value && ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSNumber class]])) {
[dict setObject:value forKey:lowerCharStr];
}else{
[dict removeObjectForKey:lowerCharStr];
}
}
然后在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中進(jìn)行邏輯判斷處理:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *str = NSStringFromSelector(sel);
if ([str containsString:@"set"]) {
class_addMethod(self, sel, (IMP)autoStudentSetter, "v@:@");
}else{
class_addMethod(self, sel, (IMP)autoStudentGetter, "@@:");
}
return YES;
}
使用點(diǎn)語法給對(duì)象進(jìn)行賦值总寒,然后進(jìn)行打印:
- (void)setData{
_model = [[StudentModel alloc]init];
_model.name = @"Samson";
_model.city = @"FuYang";
_model.country = @"China";
NSLog(@"model.name:%@,model.city:%@,model.country:%@",_model.name,_model.city,_model.country);
}
查看打印結(jié)果: