Objective-C 消息轉(zhuǎn)發(fā)

快速轉(zhuǎn)發(fā)

什么是快速轉(zhuǎn)發(fā)呢冤竹?我們假設(shè)有這么一個(gè)對(duì)象 CacheProxy峻汉,若是有未知的選擇器發(fā)送到 CacheProxy势誊,objc_msgSend 都會(huì)調(diào)用 CacheProxy 的 forwardingTargetForSelector: 方法跟啤,如果這個(gè)方法返回一個(gè)對(duì)象烫饼,那么 objc_msgSend 會(huì)試著將這個(gè)未知的選擇器發(fā)送給返回的那個(gè)對(duì)象熬甫,這就是快速轉(zhuǎn)發(fā)(fast forwarding)胰挑。那么我們可以利用這個(gè)快速轉(zhuǎn)發(fā)的特性來(lái)代理對(duì)象。

我們使用 CacheProxy 這個(gè)對(duì)象來(lái)說(shuō)明如何利用快速轉(zhuǎn)發(fā)的特性來(lái)緩存其他對(duì)象的 setter 和 getter 方法椿肩。

// CacheProxy.h
#import <Foundation/Foundation.h>
// 快速轉(zhuǎn)發(fā) :若是有未知的選擇器發(fā)送到 CacheProxy瞻颂,objc_msgSend 都會(huì)調(diào)用 CacheProxy 的 forwardingTargetForSelector: 方法,如果這個(gè)方法返回一個(gè)對(duì)象郑象,那么 objc_msgSend 會(huì)試著將這個(gè)未知的選擇器發(fā)送給返回的那個(gè)對(duì)象贡这。
@interface CacheProxy : NSProxy
// 初始化方法 ,返回類(lèi)型為 id 類(lèi)型厂榛,可以避免編譯器報(bào)錯(cuò)
- (id)initWithObject:(id)anObject properties:(NSArray *)properties;
@end

值的注意的是 CacheProxy 并不是 NSObject 的子類(lèi)盖矫,而是 NSProxy 的子類(lèi),NSProxy 是一個(gè)輕量級(jí)的根類(lèi)击奶,是為那些轉(zhuǎn)發(fā)大部分方法調(diào)用的類(lèi)而設(shè)計(jì)的辈双。CacheProxy 本身就是一個(gè)代理對(duì)象,適合成為 NSProxy 的子類(lèi)正歼。

// CacheProxy.m
#import "CacheProxy.h"
#import <objc/runtime.h>

@interface CacheProxy ()

@property (nonatomic,strong) id object;//被代理的對(duì)象
@property (nonatomic,strong) NSMutableDictionary *valueForProperty;// 用于保存被代理對(duì)象的屬性值

@end

@implementation CacheProxy

// setFoo: -> foo
// 通過(guò) selector 得到屬性名
static NSString *propertyNameForSelector(SEL selector){
// 省略代碼
}

// foo -> setFoo:
// 通過(guò)屬性名得到 selector
static SEL setterForPropertyName(NSString *property){
// 省略代碼
}

// getter 方法實(shí)現(xiàn)
static id propertyIMP(id self, SEL _cmd){
    NSString *propertyName = NSStringFromSelector(_cmd);
    id value = [[self valueForProperty] valueForKey:propertyName];
    // NSMutableDictionary 不能存儲(chǔ) nil辐马,所以使用 NSNull 來(lái)處理 nil
    if (value == [NSNull null]) {
        return nil;
    }

    if (value) {
        return value;
    }

    // 從緩存對(duì)象取不到屬性值的話(huà),那么從原對(duì)象取屬性值
    value = [[self object] valueForKey:propertyName];
    // 從原對(duì)象取屬性值之后局义,將屬性值緩存到緩存對(duì)象
    [[self valueForProperty] setValue:value forKey:propertyName];

    return value;
}

// setter 方法實(shí)現(xiàn)
static void setPropertyIMP(id self, SEL _cmd, id aValue){
    id value = [aValue copy];
    NSString *propertyName = propertyNameForSelector(_cmd);
    // 先將屬性值設(shè)置到緩存對(duì)象喜爷,再將屬性值設(shè)置到原對(duì)象
    [[self valueForProperty] setValue:(value != nil ? value : [NSNull null]) forKey:propertyName];
    [[self object] setValue:value forKey:propertyName];
}


- (id)initWithObject:(id)anObject properties:(NSArray *)properties{
    _object = anObject;
    _valueForProperty = [NSMutableDictionary new];
    // 緩存對(duì)象為 anObject 的所有屬性生成 setter 和 getter 方法
    for(NSString *property in properties){
        // 添加 getter 方法
        class_addMethod([self class], NSSelectorFromString(property), (IMP)propertyIMP, "@@:");
        // 添加 setter 方法
        class_addMethod([self class], setterForPropertyName(property), (IMP)setPropertyIMP, "v@:@");
    }
    return self;
}

// 覆寫(xiě)以下方法,CacheProxy 緩存對(duì)象對(duì)外可以被識(shí)別為 object 對(duì)象
- (NSString *)description{
    return [NSString stringWithFormat:@"%@ (%@)",[super description],self.object];
}

- (BOOL)isEqual:(id)object{
    return [self.object isEqual:object];
}

- (NSUInteger)hash{
    return [self.object hash];
}

- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.object respondsToSelector:aSelector];
}

- (BOOL)isKindOfClass:(Class)aClass{
    return [self.object isKindOfClass:aClass];
}


// 如果有未知的選擇器發(fā)送到 CacheProxy 緩存對(duì)象萄唇,在這里把所有的未知選擇器都發(fā)送給代理對(duì)象檩帐。
// 快速轉(zhuǎn)發(fā)
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.object;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.object methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation setTarget:self.object];
    [invocation invoke];
}

@end
  1. 方法 static NSString *propertyNameForSelector(SEL selector) 用于通過(guò) selector 得到屬性名。

  2. 方法 static SEL setterForPropertyName(NSString *property)用于通過(guò)屬性名得到 selector另萤。

  3. 方法 static id propertyIMP(id self, SEL _cmd) 是一個(gè) getter 方法實(shí)現(xiàn)湃密。由于 NSMutableDictionary 不能存儲(chǔ) nil诅挑,所以使用 NSNull 來(lái)處理 nil。從 CacheProxy 這個(gè)緩存對(duì)象對(duì)象取不到屬性值的話(huà)泛源,那么從被代理對(duì)象取屬性值拔妥。 從被代理對(duì)象取屬性值之后,將屬性值緩存到緩存對(duì)象达箍。

  4. 方法 static void setPropertyIMP(id self, SEL _cmd, id aValue) 是一個(gè) setter 方法實(shí)現(xiàn)没龙。 先將屬性值設(shè)置到緩存對(duì)象,再將屬性值設(shè)置到被代理對(duì)象缎玫。

  5. forwardingTargetForSelector: 方法硬纤,如果有未知的選擇器發(fā)送到 CacheProxy 緩存對(duì)象,在這里把所有的未知選擇器都發(fā)送給被代理對(duì)象赃磨。

  6. 如果被代理的對(duì)象不響應(yīng) CacheProxy 發(fā)送過(guò)來(lái)的未知選擇器筝家,那么 objc_msgSend會(huì)調(diào)用 methodSignatureForSelector: 和 forwardInvocation: 進(jìn)行普通轉(zhuǎn)發(fā)。

  7. 初始化方法 initWithObject: properties: 做的工作是代理對(duì)象生成被代理對(duì)象的屬性 setter 和 getter 方法邻辉。

  8. 由于 NSProxy 有一些方法的默認(rèn)實(shí)現(xiàn)溪王,有默認(rèn)實(shí)現(xiàn)的方法則不會(huì)被轉(zhuǎn)發(fā)到代理對(duì)象,所以需要覆寫(xiě)以下方法值骇。

- (NSString *)description{
    return [NSString stringWithFormat:@"%@ (%@)",[super description],self.object];
}

- (BOOL)isEqual:(id)object{
    return [self.object isEqual:object];
}

- (NSUInteger)hash{
    return [self.object hash];
}

- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.object respondsToSelector:aSelector];
}

- (BOOL)isKindOfClass:(Class)aClass{
    return [self.object isKindOfClass:aClass];
}

創(chuàng)建完了 CacheProxy 這個(gè)代理對(duì)象在扰,我們?cè)賱?chuàng)建一個(gè) 被代理對(duì)象 CachePerson

#import <Foundation/Foundation.h>
@interface CachePerson : NSObject
// 2個(gè)屬性
@property (nonatomic,copy) NSString *firstName;
@property (nonatomic,copy) NSString *lastName;
@end

接下來(lái)看看如何使用 CacheProxy 這個(gè)代理對(duì)象

    // 說(shuō)明 CacheProxy 如何緩存其他對(duì)象的 setter 和 getter 方法
    NSLog(@"------------------------------------------");
    id cachePerson = [[CachePerson alloc] init];
    id cacheProxy = [[CacheProxy alloc] initWithObject:cachePerson properties:@[@"firstName",@"lastName"]];
    // 設(shè)置 CacheProxy 對(duì)象的屬性, cachePerson 這個(gè)被代理對(duì)象的屬性也有值
    [cacheProxy setFirstName:@"CCCC"];
    [cacheProxy setLastName:@"DDDD"];
    NSLog(@"CacheProxy : firstName-->%@,  lastName--->%@",[cacheProxy firstName],[cacheProxy lastName]);
    NSLog(@"CachePerson : firstName-->%@,  lastName--->%@",[cachePerson firstName],[cachePerson lastName]);

    // 說(shuō)明快速轉(zhuǎn)發(fā)
    NSLog(@"------------------------------------------");
    // 只設(shè)置被代理對(duì)象 CachePerson 的屬性雷客,利用快速轉(zhuǎn)發(fā)的特性 CacheProxy 也能拿到 CachePerson 的屬性
    id cachePerson2 = [[CachePerson alloc] init];
    [cachePerson2 setFirstName:@"EEEE"];
    [cachePerson2 setLastName:@"FFFF"];

    id cacheProxy2 = [[CacheProxy alloc] initWithObject:cachePerson2 properties:@[@"firstName"]];
    NSLog(@"CacheProxy2 : firstName-->%@,  lastName--->%@",[cacheProxy2 firstName],[cacheProxy2 lastName]);
    NSLog(@"CachePerson2 : firstName-->%@,  lastName--->%@",[cachePerson2 firstName],[cachePerson2 lastName]);

運(yùn)行結(jié)果:

 ------------------------------------------
CacheProxy : firstName-->CCCC,  lastName--->DDDD
CachePerson : firstName-->CCCC,  lastName--->DDDD
 ------------------------------------------
CacheProxy2 : firstName-->EEEE,  lastName--->FFFF
CachePerson2 : firstName-->EEEE,  lastName--->FFFF

從運(yùn)行結(jié)果來(lái)看,第一個(gè)案例桥狡,我們?cè)O(shè)置了 CacheProxy 對(duì)象的屬性 firstName 和 lastName搅裙, cachePerson 這個(gè)被代理對(duì)象的屬性 firstName 和 lastName 也拿到了對(duì)應(yīng)的值。第二個(gè)案例裹芝,我們只設(shè)置被代理對(duì)象 CachePerson 的屬性 firstName 和 lastName 的值部逮,但是利用快速轉(zhuǎn)發(fā)的特性 CacheProxy 也能拿到 CachePerson 的屬性 firstName 和 lastName 的值。

普通轉(zhuǎn)發(fā)

在前面的快速轉(zhuǎn)發(fā)失效之后嫂易,也就是經(jīng)過(guò) resolveInstanceMethod: 和 forwardingTargetForSelector: 方法之后還是無(wú)法找到未知選擇器的響應(yīng)對(duì)象兄朋,那么 Runtime 就會(huì)嘗試最慢的轉(zhuǎn)發(fā)方式 forwardInvocation: 方法。

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation setTarget:self.object];
    [invocation invoke];
}

如上面代碼所示怜械,在 forwardInvocation: 方法會(huì)收到一個(gè) NSInvocation 參數(shù)颅和,這個(gè) NSInvocation 類(lèi)把目標(biāo),選擇器缕允,方法簽名和方法參數(shù)封裝在一起峡扩。我們可以在方法內(nèi)部改變 NSInvocation 的目標(biāo)然后再調(diào)用。

若是類(lèi)有實(shí)現(xiàn) forwardInvocation: 方法障本,那么也需要實(shí)現(xiàn) methodSignatureForSelector: 方法教届, NSInvocation 類(lèi)的方法簽名就是來(lái)自于這個(gè)方法响鹃。

在 forwardInvocation:方法中,我們可以看到該方法沒(méi)有任何返回值案训,但是 Runtime 會(huì)把 NSInvocation 的結(jié)果返回給最初的調(diào)用者买置。

轉(zhuǎn)發(fā)失敗

在整個(gè)消息轉(zhuǎn)發(fā)過(guò)程都沒(méi)有為未知選擇器找到響應(yīng)對(duì)象,那么接下來(lái)要怎么辦强霎?
由于 forwardInvocation:是消息轉(zhuǎn)發(fā)過(guò)程的最后一環(huán)忿项,若是它不處理這個(gè)未知選擇器的話(huà),那么就什么也不會(huì)發(fā)生脆栋,也可以利用這個(gè)特點(diǎn)來(lái)丟棄某些方法倦卖。但是 forwardInvocation: 的默認(rèn)實(shí)現(xiàn)會(huì)有一些動(dòng)作,它會(huì)調(diào)用 doesNotReconizeSelector:方法椿争,該方法會(huì)拋出 NSInvalidArgumentException怕膛。

我們平常也可以使用 doesNotReconizeSelector:方法做一些自己的事情,比如某個(gè)類(lèi)的 init 方法不允許調(diào)用秦踪,那么有如下實(shí)現(xiàn)方案褐捻。

- (id)init{
    [self doesNotReconizeSelector:_cmd];
}

要是有人調(diào)用 init 方法, Runtime 會(huì)報(bào)錯(cuò)椅邓。
其實(shí)要實(shí)現(xiàn)這個(gè)效果還可以這么做柠逞,

- (id)init{
    NSAssert(NO,"不要直接調(diào)用 init 方法");
    return nil;
}

最后值得注意的是,當(dāng)有一個(gè)類(lèi)無(wú)法響應(yīng)未知的選擇器的時(shí)候應(yīng)該在 forwardInvocation:中調(diào)用 doesNotReconizeSelector: 景馁。除非你非常清楚你想要做什么板壮,不然不要直接返回,直接返回可能會(huì)導(dǎo)致一些非常蛋疼的 bug 合住。

參考

本文是《iOS 編程實(shí)戰(zhàn)》的讀書(shū)筆記绰精,對(duì)閱讀的內(nèi)容進(jìn)行總結(jié)。當(dāng)我們看懂了之后透葛,不一定懂笨使;我們跟著書(shū)上代碼敲了一遍之后,還是不一定懂僚害;只有我們能夠把自己理解的內(nèi)容寫(xiě)下來(lái)或者通過(guò)其它方式表達(dá)出來(lái)的時(shí)候硫椰,這個(gè)才是真的懂了;

  1. demo https://github.com/junbinchencn/DynamicWork
  2. iOS編程實(shí)戰(zhàn) https://book.douban.com/subject/25976913/
  3. https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html#//apple_ref/doc/uid/TP40008048-CH105-SW2
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末萨蚕,一起剝皮案震驚了整個(gè)濱河市靶草,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌门岔,老刑警劉巖爱致,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寒随,居然都是意外死亡糠悯,警方通過(guò)查閱死者的電腦和手機(jī)帮坚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)互艾,“玉大人试和,你說(shuō)我怎么就攤上這事∪移眨” “怎么了阅悍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)昨稼。 經(jīng)常有香客問(wèn)我节视,道長(zhǎng),這世上最難降的妖魔是什么假栓? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任寻行,我火速辦了婚禮,結(jié)果婚禮上匾荆,老公的妹妹穿的比我還像新娘拌蜘。我一直安慰自己,他們只是感情好牙丽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布简卧。 她就那樣靜靜地躺著,像睡著了一般烤芦。 火紅的嫁衣襯著肌膚如雪举娩。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天构罗,我揣著相機(jī)與錄音晓铆,去河邊找鬼。 笑死绰播,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尚困。 我是一名探鬼主播蠢箩,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼事甜!你這毒婦竟也來(lái)了谬泌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤逻谦,失蹤者是張志新(化名)和其女友劉穎掌实,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體邦马,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贱鼻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年宴卖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片邻悬。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡症昏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出父丰,到底是詐尸還是另有隱情肝谭,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布蛾扇,位于F島的核電站攘烛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏镀首。R本人自食惡果不足惜坟漱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蘑斧。 院中可真熱鬧靖秩,春花似錦、人聲如沸竖瘾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)捕传。三九已至惠拭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庸论,已是汗流浹背职辅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留聂示,地道東北人域携。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鱼喉,于是被迫代替她去往敵國(guó)和親秀鞭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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