《Effective Objective-C 2.0》- 12:理解消息轉(zhuǎn)發(fā)機制

1. 消息轉(zhuǎn)發(fā)機制

當對象接收到無法解讀的消息后,就會啟動“消息轉(zhuǎn)發(fā)”機制军拟,開發(fā)者可經(jīng)由此過程告訴對象應(yīng)該如何處理未知消息郊愧。

消息轉(zhuǎn)發(fā)分為兩大階段

  • 第一階段:先征詢接收者所屬的類晓铆,看其是否能動態(tài)添加方法筷黔,以處理當前這個“未知的選擇器”,這叫做“動態(tài)方法解析”(dynamic method resolution)刊头。
  • 第二階段:涉及“完整的消息轉(zhuǎn)發(fā)機制”(full forwarding mechanism)黍瞧。
    運行時系統(tǒng)會請求接收者以“動態(tài)新增方法”之外的手段來處理與消息相關(guān)的方法調(diào)用,這又細分為兩小步原杂。首先印颤,請接收者看看有沒有其他對象能處理這條消息。若有穿肄,則運行期系統(tǒng)會把消息轉(zhuǎn)給那個對象年局,于是消息轉(zhuǎn)發(fā)過程結(jié)束。若沒有“備援的接收者”咸产,則啟動完整的消息轉(zhuǎn)發(fā)機制矢否,運行時系統(tǒng)會把與消息有關(guān)的全部細節(jié)都封裝到NSInvocation對象中,再給接收者最后一次機會脑溢,令其設(shè)法解決當前還沒處理的這條消息僵朗。

2. 動態(tài)方法解析

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

+ (BOOL)resolveInstanceMethod:(SEL)selector

解釋:selector是未知的選擇器屑彻,返回值為Boolean類型验庙,表示這個類是否能新增一個實例方法用以處理此選擇器。

在繼續(xù)往下執(zhí)行轉(zhuǎn)發(fā)之前社牲,本類有機會新增一個處理未知選擇器的方法粪薛,便是通過調(diào)用“resolveInstanceMethod:”或“resolveClassMethod:”方法來實現(xiàn)的。
但是搏恤,使用這種辦法有個前提:相關(guān)方法的實現(xiàn)代碼已經(jīng)寫好违寿,只等著運行的時候動態(tài)插在類里面就可以了让禀。此方案常用來實現(xiàn)@dynamic屬性。

id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);

+ (BOOL)resolveInstanceMethod:(SEL)selector{
    NSString *selectorString = NSStringFromSelector(selector);// 將選擇器轉(zhuǎn)換為字符串
    if(/* selector is from a @dynamic property */){ // 使用了@dynamic屬性
        if([selectorString hasPrefix:@"set"]){
            class_addMethod(self, selector, (IMP)autoDictionarySetter, "V@:@");
        } else {
            class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    return [super resolveInstanceMethod:selector];
}

3. 備援接收者

在第二階段的第一小步中陨界,運行期系統(tǒng)會問未知的選擇器能不能把這條消息轉(zhuǎn)發(fā)給其他接收者來處理。與該步驟對應(yīng)的處理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

解釋:selector代表未知的選擇器痛阻,若當前接收者能找到備援對象菌瘪,則將其返回,若找不到阱当,就返回nil俏扩。
通過此方案,可以用“組合”(composition)來模擬出“多重繼承”(multiple inheritance)的某些特性弊添。
注意:開發(fā)者無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息录淡。若是想在發(fā)送給備援接收者之前先修改消息內(nèi)容,那就得通過完整的消息轉(zhuǎn)發(fā)機制來做了

4. 完整的消息轉(zhuǎn)發(fā)

若沒有“備援的接收者”油坝,則啟動完整的消息轉(zhuǎn)發(fā)機制嫉戚,運行時系統(tǒng)會把與尚未處理的那條消息有關(guān)的全部細節(jié)都封裝到NSInvocation對象中。在觸發(fā)NSInvocation對象時澈圈,“消息派發(fā)系統(tǒng)”(message-dispatch system)將親自出馬彬檀,把消息指派給目標對象。如下:

- (void)forwardInvocation:(NSInvocation*)invacation

此方法比較有用的實現(xiàn)方式為:在觸發(fā)消息前瞬女,先以某種方式改變消息內(nèi)容窍帝,比如追加另外一個參數(shù),或是改換選擇器诽偷,等等坤学。

5. 以完整的例子演示動態(tài)方法解析

EOCAutoDictionary.h

#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.m

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

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

@implementation EOCAutoDictionary

// @dynamic會阻止編譯器自動生成相關(guān)的存取方法,而由開發(fā)者自己創(chuàng)建存取方法
@dynamic string, number, date, opaqueObject;

- (id)init{
    if (self = [super init]) {
        _backingStore = [NSMutableDictionary new]; // 延遲加載
    }
    return self;
}

// 動態(tài)添加新方法
+ (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;
}

// getter函數(shù)
id autoDictionaryGetter(id self, SEL _cmd){
    // 從EOCAutoDictionary對象獲取backingStore字典
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    // 將選擇器轉(zhuǎn)換為字符串报慕,并將其設(shè)為key
    NSString *key = NSStringFromSelector(_cmd);
    
    // 返回backingStore字典中key所對應(yīng)的值
    return [backingStore objectForKey:key];
}

// setter函數(shù)
void autoDictionarySetter(id self, SEL _cmd, id value){
    // 從EOCAutoDictionary對象獲取backingStore字典
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    // 將選擇器轉(zhuǎn)換為字符串深浮,并將其拷貝為可變字符串
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    // 移除key中尾部的“:”
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    
    // 移除key中前面的“set”
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    // 取出現(xiàn)有的key中的首字母,將其小寫化并替代掉原來的首字母
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    // 根據(jù)key給backingStore存儲相關(guān)的值
    if (value) {
        [backingStore setObject:value forKey:key];
    }else{
        [backingStore removeObjectForKey:key];
    }
    
}
@end

main函數(shù):

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        EOCAutoDictionary *autoDict = [EOCAutoDictionary new];
        // autoDict.date == [autoDict setDate]
        // 由于接收者沒有相應(yīng)的方法可調(diào)用卖子,因為@dynamic特性略号,所以可以動態(tài)新增方法
        autoDict.date = [NSDate dateWithTimeIntervalSince1970:3140907998];
        NSLog(@"%@",autoDict.date);
    }
    return 0;
}

輸出結(jié)果為:

2018-08-16 15:29:25.552 
2071-09-13 02:26:38 +0000

總結(jié):要想添加新屬性,只需要用@property來定義洋闽,并將其聲明為@dynamic即可玄柠。

要點

  • 若對象無法響應(yīng)某個選擇器,則進入消息轉(zhuǎn)發(fā)流程诫舅。
  • 通過運行期的動態(tài)方法解析功能羽利,我們可以在需要用到某個方法時再將其加入類中。
  • 對象可以把其無法解讀的某些選擇器轉(zhuǎn)交給其他對象(備援接收者)來處理刊懈。
  • 經(jīng)過上述兩步之后这弧,如果還是沒辦法處理選擇器娃闲,那就啟動完整的消息轉(zhuǎn)發(fā)機制。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匾浪,一起剝皮案震驚了整個濱河市皇帮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛋辈,老刑警劉巖属拾,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異冷溶,居然都是意外死亡渐白,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門逞频,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纯衍,“玉大人,你說我怎么就攤上這事苗胀〗笾睿” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵基协,是天一觀的道長励堡。 經(jīng)常有香客問我,道長堡掏,這世上最難降的妖魔是什么应结? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮泉唁,結(jié)果婚禮上鹅龄,老公的妹妹穿的比我還像新娘。我一直安慰自己亭畜,他們只是感情好扮休,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拴鸵,像睡著了一般玷坠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上劲藐,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天八堡,我揣著相機與錄音,去河邊找鬼聘芜。 笑死兄渺,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的汰现。 我是一名探鬼主播挂谍,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼叔壤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了口叙?” 一聲冷哼從身側(cè)響起炼绘,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎妄田,沒想到半個月后饭望,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡形庭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厌漂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萨醒。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖苇倡,靈堂內(nèi)的尸體忽然破棺而出富纸,到底是詐尸還是另有隱情,我是刑警寧澤旨椒,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布晓褪,位于F島的核電站,受9級特大地震影響综慎,放射性物質(zhì)發(fā)生泄漏涣仿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一示惊、第九天 我趴在偏房一處隱蔽的房頂上張望好港。 院中可真熱鬧,春花似錦米罚、人聲如沸钧汹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拔莱。三九已至,卻和暖如春隘竭,著一層夾襖步出監(jiān)牢的瞬間塘秦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工动看, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嗤形,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓弧圆,卻偏偏與公主長得像赋兵,于是被迫代替她去往敵國和親笔咽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

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