iOS的消息轉(zhuǎn)發(fā)機(jī)制

若想令類能理解某條消息,我們必須以程序碼實(shí)現(xiàn)出對(duì)應(yīng)的方法才行。但是,編譯器在編譯時(shí)還無(wú)法知道類中有沒(méi)有對(duì)某個(gè)方法的實(shí)現(xiàn)。當(dāng)對(duì)象接收到無(wú)法解讀的消息后,就會(huì)啟動(dòng)“消息轉(zhuǎn)發(fā)”(message forwarding)機(jī)制,我們可以經(jīng)由此過(guò)程告訴對(duì)象應(yīng)該如何處理未知消息。
如果控制臺(tái)中看到下面的這種提示,那就說(shuō)明你曾向某個(gè)對(duì)象發(fā)送過(guò)一條其無(wú)法解讀的消息,從而啟動(dòng)了消息轉(zhuǎn)發(fā)機(jī)制,并將此消息轉(zhuǎn)發(fā)給了NSObject的默認(rèn)實(shí)現(xiàn)。



上面這段異常信息是由NSObject的 “doesNotRecognizeSelector:”方法所拋出的,此異常表明,消息接收者的類型是 ViewController,而該接收者無(wú)法理解名為 doSomething 的選擇子。
在本例中,消息轉(zhuǎn)發(fā)過(guò)程以程序崩潰而告終,不過(guò),開(kāi)發(fā)者在編寫(xiě)自己的類時(shí),可于轉(zhuǎn)發(fā)過(guò)程中設(shè)置掛鉤,用以執(zhí)行預(yù)定的邏輯,而不使應(yīng)用程序崩潰。

消息轉(zhuǎn)發(fā)機(jī)制分為兩大階段。第一階段先征詢接收者所屬的類,看其是否能動(dòng)態(tài)添加方法,以處理當(dāng)前這個(gè)“未知的選擇子”(unknow selector),這叫做“動(dòng)態(tài)方法解析”(dynamic method resolution)。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”(full forwarding mechanism).

一、動(dòng)態(tài)方法解析


如果該方法是實(shí)例方法,對(duì)象在接收到無(wú)法解讀的消息后,首先將調(diào)用其所屬類的 resolveInstanceMethod: 類方法。sel就是未知的選擇子,該方法返回一個(gè)boolean類型,表示這個(gè)類是否能新增一個(gè)實(shí)例方法處理此選擇子。



如果該方法是類方法,那么運(yùn)行期系統(tǒng)就會(huì)調(diào)用resolveClassMethod:類方法。

使用上面方法的前提是:相關(guān)方法的實(shí)現(xiàn)代碼已經(jīng)寫(xiě)好,只等著運(yùn)行的時(shí)候動(dòng)態(tài)插在類里面就可以了。

下面還是以上面的button為例,為其實(shí)現(xiàn)動(dòng)態(tài)方法解析。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"doSomething"]) {
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}
void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"動(dòng)態(tài)添加了方法\"%@\" ,防止程序crash", NSStringFromSelector(_cmd));
}

控制臺(tái)打印如下:程序不會(huì)crash。


備援接收者

當(dāng)前接收者還有第二次機(jī)會(huì)能處理未知的選擇子,在這一步中,運(yùn)行期系統(tǒng)會(huì)問(wèn)它:能不能把這條消息轉(zhuǎn)發(fā)給其他接收者來(lái)處理。與該步驟對(duì)應(yīng)的處理方法如下:


方法參數(shù)代表未知選擇子,若當(dāng)前接收者能找到備援對(duì)象,則將其返回,若找不到,就返回nil。通過(guò)此方案,我們可以用“組合(composition)”來(lái)模擬“多重繼承”的某些特性。在一個(gè)對(duì)象內(nèi)部,可能還有一系列其他對(duì)象,該對(duì)象可經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對(duì)象返回,這樣的話,在外界看來(lái),好像是該對(duì)象親自處理了這些消息。

請(qǐng)注意,我們無(wú)法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息。若是想在發(fā)送給備援接收者之前先修改消息內(nèi)容,那就得通過(guò)完整的消息轉(zhuǎn)發(fā)機(jī)制來(lái)做。

下面還是以上面的button為例,為其實(shí)現(xiàn)備援接收者。

聲明一個(gè)備援接收者的類
#import <Foundation/Foundation.h>

@interface MyForwardingTargetClass : NSObject

@end

#import "MyForwardingTargetClass.h"

@implementation MyForwardingTargetClass

// 不需要在.h中聲明,運(yùn)行時(shí)會(huì)動(dòng)態(tài)查找類中是否實(shí)現(xiàn)該方法
- (void)doSomething {
    NSLog(@"備援接受者的方法調(diào)用了,程序沒(méi)有crash!!!");
}

@end

在VC中實(shí)現(xiàn)備援接收者的處理方法:

// 消息轉(zhuǎn)發(fā)機(jī)制 第一階段:備援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 備援接收者 只需要在.m中實(shí)現(xiàn)doSomething就可以防止crash
    if ([NSStringFromSelector(aSelector) isEqualToString:@"doSomething"]) {
        return [MyForwardingTargetClass new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

控制臺(tái)打印如下,程序沒(méi)有crash.


二、完整的消息轉(zhuǎn)發(fā)機(jī)制

如果轉(zhuǎn)發(fā)算法已經(jīng)到了這一步,那只能啟動(dò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)用下列方法來(lái)轉(zhuǎn)發(fā)消息:


這個(gè)方法可以實(shí)現(xiàn)的很簡(jiǎn)單:只需要改變調(diào)用目標(biāo),使消息在新的目標(biāo)上得以調(diào)用即可。然而這樣實(shí)現(xiàn)出來(lái)的方法與“備援接收者”方案所實(shí)現(xiàn)的等效,一般很少采用這么簡(jiǎn)單的實(shí)現(xiàn)方式。
比較有用的實(shí)現(xiàn)方式為:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另一個(gè)參數(shù),或者改變選擇子等等。
實(shí)現(xiàn)此方法時(shí),若發(fā)現(xiàn)某調(diào)用操作不應(yīng)由本類處理,則需調(diào)用超類的同名方法。這樣的話,繼承體系中的每個(gè)類都有機(jī)會(huì)處理此調(diào)用請(qǐng)求,直至NSObject。如果最后調(diào)用了NSObject類的方法,那么該方法還會(huì)繼而調(diào)用“doesNotRecognizeSelector:”以拋出異常,此異常表明選擇子最終未能得到處理。

下面還是以上面的button為例,為其實(shí)現(xiàn)完整的消息轉(zhuǎn)發(fā)機(jī)制。此處先簡(jiǎn)單的實(shí)現(xiàn)下(和備援接收者實(shí)現(xiàn)方案等效):

創(chuàng)建一個(gè)類,處理vc不能處理的方法
#import <Foundation/Foundation.h>
@interface MethodCrashClass : NSObject

- (void)methodCrash:(NSInvocation *)invocation;

@end

#import "MethodCrashClass.h"
@implementation MethodCrashClass

- (void)methodCrash:(NSInvocation *)invocation {
    NSLog(@"在類:%@中 未實(shí)現(xiàn)該方法:%@",NSStringFromClass([invocation.target class]),NSStringFromSelector(invocation.selector));
}

@end

控制臺(tái)打印如下,程序沒(méi)有crash。

那么問(wèn)題來(lái)了!這些方法是在VC中實(shí)現(xiàn)的,如果我們想要給每個(gè)類都添加一個(gè)防止crash的方法呢?顯然這樣添加不是一個(gè)很好的選擇。

解決方案:

//創(chuàng)建NSObject的分類
#import <Foundation/Foundation.h>
@interface NSObject (crashLog)

@end

#import "NSObject+crashLog.h"
@implementation NSObject (crashLog)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 方法簽名
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"在類:%@中 未實(shí)現(xiàn)該方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}

@end

控制臺(tái)打印如下,程序沒(méi)有crash。

因?yàn)樵赾ategory中復(fù)寫(xiě)了父類的方法,會(huì)出現(xiàn)下面的警告,



解決辦法就是在Xcode的Build Phases中的資源文件里,在對(duì)應(yīng)的文件后面 -w ,忽略所有警告。



此處還有一點(diǎn)需要解釋的,就是在方法簽名中的Types:"v@:@",這些符號(hào)是什么意思呢?
其實(shí)這些符號(hào)就是返回值和方法參數(shù)對(duì)應(yīng)的類型??稍赬code中的開(kāi)發(fā)者文檔中搜索Type Encodings就可看到符號(hào)對(duì)應(yīng)的含義,此處不再列舉了。

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

接收者在每一步中均有機(jī)會(huì)處理消息。步驟越往后,處理消息的代價(jià)越大。最好能在第一步處理完,這樣運(yùn)行期系統(tǒng)可以把此方法緩存起來(lái)。如果這個(gè)類的實(shí)例稍后還會(huì)收到同名選擇子,則無(wú)須啟動(dòng)消息轉(zhuǎn)發(fā)流程。
若想在第三步里把消息轉(zhuǎn)發(fā)給備援接收者,還不如把轉(zhuǎn)發(fā)操作提前到第二步。因?yàn)榈谌街皇切薷牧苏{(diào)用目標(biāo),這項(xiàng)改動(dòng)放在第二步執(zhí)行會(huì)更簡(jiǎn)單,不然的話,還要?jiǎng)?chuàng)建并處理完整的NSIncocation。


demo放在GitHub上了,有需要的可以download下來(lái)查看.


可以利用消息轉(zhuǎn)發(fā)機(jī)制的三個(gè)步驟,選擇哪一步去改造比較合適呢?

這里我們選擇了第二步forwardingTargetForSelector。引用 《大白健康系統(tǒng)—iOS APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng)》 的分析:

  • resolveInstanceMethod 需要在類的本身上動(dòng)態(tài)添加它本身不存在的方法,這些方法對(duì)于該類本身來(lái)說(shuō)冗余的。
  • forwardInvocation 可以通過(guò) NSInvocation 的形式將消息轉(zhuǎn)發(fā)給多個(gè)對(duì)象,但是其開(kāi)銷較大,需要?jiǎng)?chuàng)建新的 NSInvocation 對(duì)象,并且 forwardInvocation 的函數(shù)經(jīng)常被使用者調(diào)用,來(lái)做多層消息轉(zhuǎn)發(fā)選擇機(jī)制,不適合多次重寫(xiě)。
  • forwardingTargetForSelector 可以將消息轉(zhuǎn)發(fā)給一個(gè)對(duì)象,開(kāi)銷較小,并且被重寫(xiě)的概率較低,適合重寫(xiě)。

千里之行,始于足下。

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

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