消息轉(zhuǎn)發(fā)機制(message forwarding)及其應(yīng)用場景

引言:OC是一種消息語言,OC對象調(diào)用方法,就是給對象發(fā)送消息,這個過程稱為消息傳遞,那如果對象接收到了無法解讀的消息,這時候要怎么處理呢?此時就用到了OC中的消息轉(zhuǎn)發(fā)機制(message forwarding)。本文分為兩部分,第一部分介紹消息轉(zhuǎn)發(fā)機制的過程,第二部分介紹消息轉(zhuǎn)發(fā)機制的應(yīng)用場景。

一.消息轉(zhuǎn)發(fā)機制過程:

消息轉(zhuǎn)發(fā)一共有三步:

1.動態(tài)方法解析(Dynamic Method Resolution):

+ (BOOL)resolveInstanceMethod:(SEL)selector; ①

+ (BOOL)resolveClassMethod:(SEL)selector;②

如果對象收到無法解讀的消息,首先會調(diào)用對象所屬類上述兩個類方法之一,詢問是否能夠動態(tài)添加無法解讀的selector。上述兩個類方法分別對應(yīng)selector為對象方法和類方法的情況。這兩個方法返回值為BOOL,表示是否能新增一個方法來處理此選擇子。

代碼示例:

    People*people = [[People alloc]init];

    [people performSelector:NSSelectorFromString(@"tonightEatChicken")];

People類的實例對象people執(zhí)行tonightEatChicken方法,而People類中并沒有該方法的實現(xiàn),如果不做任何處理,程序運行,將會崩潰。而如果我們使用動態(tài)方法解析做如下處理:

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    NSString *selString = NSStringFromSelector(sel);

    if([selString isEqualToString:@"tonightEatChicken"]) {

        //為當(dāng)前類添加此方法

        class_addMethod(self, sel , (IMP)tonightEatChicken, "v@:@");

        returnYES;

    }

    return [super resolveInstanceMethod:sel];

}

void tonightEatChicken(id self,SEL_cmd) {

    NSLog(@"%@--%@今晚吃雞",self,NSStringFromSelector(_cmd));

}

再運行,程序正常運行,控制臺輸出<People: 0x6000023e06c0>--tonightEatChicken今晚吃雞。
這是消息轉(zhuǎn)發(fā)機制的第一步,值得注意的是:這一步驟中動態(tài)添加的方法,將會被運行時系統(tǒng)緩存,如果People類的實例稍后接收到同樣的選擇子,則不會進(jìn)入消息轉(zhuǎn)發(fā)流程,直接在消息發(fā)送階段完成,這可以理解為runtime系統(tǒng)的優(yōu)化工作,減少了方法查找的步驟。

2.備援接收者(Replacement Receiver)

如果當(dāng)前接收者沒有在第一步動態(tài)方法解析中進(jìn)行處理,則還有第二次機會處理該selector,具體方法如下:

- (id)forwardingTargetForSelector:(SEL)selector;

這個方法同樣由NSObject聲明,所有繼承于NSObject的類,都可以實現(xiàn)這個方法。這個方法需要返回可以接收該selector的類對象或者實例對象,如果該selector為類方法,則返回類對象,否則,返回實例。
具體實現(xiàn)如下,我們新建Soldier類并實現(xiàn)該選擇子對象的方法:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"tonightEatChicken"]) {
        //    //return [Soldier class];aSelector為類方法,則返回類對象
        return [[Soldier alloc]init];//aSelector為實例方法,則返回類對象
    }
    return [super forwardingTargetForSelector:aSelector];
}
#import "Soldier.h"

@implementation Soldier

- (void)tonightEatChicken {
    NSLog(@"士兵今晚吃雞");
}

@end

程序成功運行并輸出"士兵今晚吃雞"。
其實在這一步,程序員能操作的就是改變消息的接收對象,這種方式可以模擬多重繼承。OC是不支持多重繼承的,利用消息轉(zhuǎn)發(fā)可以變相的實現(xiàn)。外界看起來,似乎是一個類同時實現(xiàn)了兩個類的某個功能,其實只是利用了消息轉(zhuǎn)發(fā)。

3.完整的消息轉(zhuǎn)發(fā)機制(Full Forwarding Mechanism)

如果前兩步都沒有處理,那么來到第三步,這一步系統(tǒng)會創(chuàng)建一個NSInvocation對象把這個消息的所有信息(包括target,selector,參數(shù)以及返回值)包裝起來。并通過- (void)forwardInvocation:(NSInvocation *)anInvocation方法,把包裝好的NSInvocation拋出來。
但是在創(chuàng)建NSInvocation對象之前,需要前獲取這個消息的方法簽名,通過

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"tonightEatChicken"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [NSMethodSignature methodSignatureForSelector:aSelector];
}

然后再實現(xiàn)

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[[Soldier alloc]init]];
    NSLog(@"%@",anInvocation);
}

如此,就會將此消息轉(zhuǎn)發(fā)給Soldier,控制臺會打印出“士兵今晚吃雞”,實現(xiàn)和第二步一樣的效果。

小結(jié):

接收者在每一步均有機會處理消息,步驟越往后,處理消息的代價越大。最好能在第一步就處理完,這樣的話,運行期系統(tǒng)就可以將此方法緩存起來,如果該類的實例稍后收到同名選擇子,就無須啟動消息轉(zhuǎn)發(fā)流程。如果只是想改變消息的接收者,那么在第三步操作不如在第二步操作。相對于第二步,第三步還會創(chuàng)建并處理完整的NSInvocation。

二.應(yīng)用場景:

了解了技術(shù)的原理,就要考慮下,這個東西能用來干啥。下面介紹下書上和網(wǎng)絡(luò)上有關(guān)消息轉(zhuǎn)發(fā)的應(yīng)用場景:

1.JSPatch

JSPatch是一個熱修復(fù)的第三方開源庫。它的實現(xiàn)原理就是利用了消息轉(zhuǎn)發(fā)機制。
具體來說,JSPatch是利用了第三步的NSInvocation對象,因為在消息轉(zhuǎn)發(fā)的第一步和第二步,我們只能獲取消息的選擇子,而在第三步,我們可以通過NSInvocation獲取當(dāng)前消息的所有內(nèi)容(接收者,選擇子,參數(shù)值)。因此可以在第三步,獲取參數(shù)值。
JSPatch具體是怎么做的呢?JSPatch 的基本原理就是:JS 傳遞字符串給 OC,OC 通過 Runtime 接口調(diào)用和替換 OC 方法。

2.實現(xiàn)屬性的自動化存取

這里模仿實現(xiàn)一個《Effective Objective-C 2.0》書中描述的一個完整的例子:
下面示范如何用動態(tài)方法解析來實現(xiàn)@dynamic屬性。實現(xiàn)一個“字典”對象,內(nèi)部可以用字典存取其他對象,但是存取方式,要通過屬性的set和get方式來實現(xiàn)。開發(fā)者只需要聲明屬性,并將屬性聲明為@dynamic。這樣運行時系統(tǒng)就不會自動為屬性生成相應(yīng)的set和get方法,需要開發(fā)者自己去實現(xiàn)。如果屬性比較少,我們可以手動書寫相應(yīng)的存取方法:

#import <Foundation/Foundation.h>
@class People;

@interface MFExampleDictionary : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, strong) People *people;

@end
#import "MFExampleDictionary.h"

@interface MFExampleDictionary ()
@property (nonatomic, strong) NSMutableDictionary *storeDictionary;
@end

@implementation MFExampleDictionary
@dynamic name,people;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _storeDictionary = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)setName:(NSString *)name {
//    使用屬性的set方法名為key -> setName:
    NSString *key = NSStringFromSelector(_cmd);
    NSLog(@"%@",key);
    [_storeDictionary setObject:name forKey:key];
}

- (NSString *)name {
//    使用屬性的set方法名為key -> setName:
    NSString *get = NSStringFromSelector(_cmd);
    NSString *key = getToSet(get);
    NSLog(@"%@",key);
    return [_storeDictionary objectForKey:key];
}

- (void)setPeople:(People *)people {
    NSString *key = NSStringFromSelector(_cmd);
    [_storeDictionary setObject:people forKey:key];
}

- (People *)people {
    NSString *get = NSStringFromSelector(_cmd);
    NSString *key = getToSet(get);
    NSLog(@"%@",key);
    return [_storeDictionary objectForKey:key];
}

NSString *getToSet(NSString *get) {
    NSString *firstChar = [get substringToIndex:1];
    NSString *upString = [get stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[firstChar uppercaseString]];
    NSString *setString = [NSString stringWithFormat:@"set%@:",upString];
    return setString;
}

@end

如果需要存取的屬性多達(dá)幾百個呢?我們就需要編寫大量的存取的方法。這時候自動轉(zhuǎn)發(fā)機制就可以為我們所用了。直接看代碼(頭文件代碼不變):

#import "MFExampleDictionary.h"
#import <objc/message.h>

@interface MFExampleDictionary ()
@property (nonatomic, strong) NSMutableDictionary *storeDictionary;
@end

@implementation MFExampleDictionary
@dynamic name,people;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _storeDictionary = [NSMutableDictionary dictionary];
    }
    return self;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        class_addMethod([self class], sel, (IMP)setMethod, "v@:@");
    }else{
        class_addMethod([self class], sel, (IMP)getMethod, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}

void setMethod(MFExampleDictionary *self,SEL _cmd,id value) {
    NSString *key = NSStringFromSelector(_cmd);
    NSLog(@"%@",key);
    [self.storeDictionary setObject:value forKey:key];
}

id getMethod(MFExampleDictionary *self,SEL _cmd) {
    NSString *get = NSStringFromSelector(_cmd);
    NSString *key = getToSet(get);
    NSLog(@"%@",key);
    return [self.storeDictionary objectForKey:key];
}

NSString *getToSet(NSString *get) {
    NSString *firstChar = [get substringToIndex:1];
    NSString *upString = [get stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[firstChar uppercaseString]];
    NSString *setString = [NSString stringWithFormat:@"set%@:",upString];
    return setString;
}

@end

我們可以看到,利用消息轉(zhuǎn)發(fā),完成了這種設(shè)計,并且減少了代碼量。
在iOS的CoreAnimation框架中CALayer類就用了與本例相似的實現(xiàn)方式,這使得CALyer成為“兼容于鍵值編碼的”容器類,也就是說,能夠向里面隨意添加屬性,然后以鍵值對的形式來訪問。于是開發(fā)者就可以向其中新增自定義的屬性了,這些屬性的存儲工作由基類直接負(fù)責(zé),開發(fā)者只需要在CALyer的子類中定義新屬性即可。
tips:這個用法主要參考書中描述的用法,其實我個人有點迷惑,既然是存儲是數(shù)據(jù),為什么一定要在對象內(nèi)部放一個字典的方式來解決呢?直接用屬性對應(yīng)的實例變量來存儲豈不是更好?如果需要以字典的形式輸出,完全可以用模型轉(zhuǎn)字典的方式來代替完成。所以對應(yīng)這種用法的必要性有點質(zhì)疑,如果有哪位同學(xué)有不一樣的想法,歡迎留言指點!

3.模擬多重繼承

模擬多重繼承,其實就是利用第二步和第三步來實現(xiàn)的。此種應(yīng)用場景也不常見,花里胡哨,個人感覺有點雞肋(=、=)。

小結(jié):

上述的原理,我們能這么干,說白了,還是蘋果爸爸暴露出來的API,蘋果爸爸給我們什么,我們用什么。值得思考的一點是,消息轉(zhuǎn)發(fā)機制有什么作用呢?Apple為什么要這么設(shè)計呢?防止收到未知消息而崩潰嗎?若是為了防止崩潰,必須提前知道哪些方法沒有被實現(xiàn),那么既然已經(jīng)知道了,程序員在編程的時候在相應(yīng)的類添加一下方法實現(xiàn)不就行了嗎?為什么還要多此一舉呢?
關(guān)于消息轉(zhuǎn)發(fā)的應(yīng)用場景目前就介紹這么多,我看網(wǎng)上有博客說還可以用來實現(xiàn)“多重代理”,這個有待考證。
如果哪位朋友關(guān)于消息轉(zhuǎn)發(fā)有更深的認(rèn)識,歡迎指教!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容