Objective-C消息轉(zhuǎn)發(fā)機(jī)制

前言:最近一直在研讀《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è)方法名未知的警告:

屏幕快照 2017-08-01 下午4.14.13.png

這是因?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é)果:

屏幕快照 2017-08-01 下午5.18.32.png

注:我們無法操作經(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:“菊卷,以拋出異常,表明選擇器最終未能得到處理宝剖。如圖:

屏幕快照 2017-08-01 下午5.59.35.png

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

屏幕快照 2017-08-01 下午6.03.04.png

上圖描述了消息轉(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é)果:

屏幕快照 2017-08-02 上午10.22.36.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市理肺,隨后出現(xiàn)的幾起案子摄闸,更是在濱河造成了極大的恐慌,老刑警劉巖妹萨,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件年枕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡乎完,警方通過查閱死者的電腦和手機(jī)熏兄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人摩桶,你說我怎么就攤上這事桥状。” “怎么了硝清?”我有些...
    開封第一講書人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵辅斟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我芦拿,道長(zhǎng)士飒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任防嗡,我火速辦了婚禮变汪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蚁趁。我一直安慰自己裙盾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開白布他嫡。 她就那樣靜靜地躺著番官,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钢属。 梳的紋絲不亂的頭發(fā)上徘熔,一...
    開封第一講書人閱讀 52,874評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音淆党,去河邊找鬼酷师。 笑死,一個(gè)胖子當(dāng)著我的面吹牛染乌,可吹牛的內(nèi)容都是我干的山孔。 我是一名探鬼主播,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼荷憋,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼台颠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起勒庄,我...
    開封第一講書人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤串前,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后实蔽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蓬衡,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡庭瑰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年椒袍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了衔瓮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漆腌。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖阶冈,靈堂內(nèi)的尸體忽然破棺而出闷尿,到底是詐尸還是另有隱情,我是刑警寧澤女坑,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布填具,位于F島的核電站,受9級(jí)特大地震影響匆骗,放射性物質(zhì)發(fā)生泄漏劳景。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一碉就、第九天 我趴在偏房一處隱蔽的房頂上張望盟广。 院中可真熱鬧,春花似錦瓮钥、人聲如沸筋量。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桨武。三九已至,卻和暖如春锈津,著一層夾襖步出監(jiān)牢的瞬間呀酸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來泰國打工琼梆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留性誉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓茎杂,卻偏偏與公主長(zhǎng)得像错览,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蛉顽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361

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