iOS的消息轉發機制

若想令類能理解某條消息,我們必須以程序碼實現出對應的方法才行。但是,編譯器在編譯時還無法知道類中有沒有對某個方法的實現。當對象接收到無法解讀的消息后,就會啟動“消息轉發”(message forwarding)機制,我們可以經由此過程告訴對象應該如何處理未知消息。
如果控制臺中看到下面的這種提示,那就說明你曾向某個對象發送過一條其無法解讀的消息,從而啟動了消息轉發機制,并將此消息轉發給了NSObject的默認實現。



上面這段異常信息是由NSObject的 “doesNotRecognizeSelector:”方法所拋出的,此異常表明,消息接收者的類型是 ViewController,而該接收者無法理解名為 doSomething 的選擇子。
在本例中,消息轉發過程以程序崩潰而告終,不過,開發者在編寫自己的類時,可于轉發過程中設置掛鉤,用以執行預定的邏輯,而不使應用程序崩潰。

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

一、動態方法解析


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



如果該方法是類方法,那么運行期系統就會調用resolveClassMethod:類方法。

使用上面方法的前提是:相關方法的實現代碼已經寫好,只等著運行的時候動態插在類里面就可以了。

下面還是以上面的button為例,為其實現動態方法解析。

+ (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(@"動態添加了方法\"%@\" ,防止程序crash", NSStringFromSelector(_cmd));
}

控制臺打印如下:程序不會crash。


備援接收者

當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條消息轉發給其他接收者來處理。與該步驟對應的處理方法如下:


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

請注意,我們無法操作經由這一步所轉發的消息。若是想在發送給備援接收者之前先修改消息內容,那就得通過完整的消息轉發機制來做。

下面還是以上面的button為例,為其實現備援接收者。

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

@interface MyForwardingTargetClass : NSObject

@end

#import "MyForwardingTargetClass.h"

@implementation MyForwardingTargetClass

// 不需要在.h中聲明,運行時會動態查找類中是否實現該方法
- (void)doSomething {
    NSLog(@"備援接受者的方法調用了,程序沒有crash!!!");
}

@end

在VC中實現備援接收者的處理方法:

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

控制臺打印如下,程序沒有crash.


二、完整的消息轉發機制

如果轉發算法已經到了這一步,那只能啟動完整的消息轉發機制了。首先創建NSInvocation 對象,把與尚未處理的那條消息有關的全部細節都封于其中。此對象包含選擇子(目標 target)參數。在觸發NSInvocation對象時,“消息派發系統”將親自出馬,把消息指派給目標對象。

此步驟會調用下列方法來轉發消息:


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

下面還是以上面的button為例,為其實現完整的消息轉發機制。此處先簡單的實現下(和備援接收者實現方案等效):

創建一個類,處理vc不能處理的方法
#import <Foundation/Foundation.h>
@interface MethodCrashClass : NSObject

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

@end

#import "MethodCrashClass.h"
@implementation MethodCrashClass

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

@end

控制臺打印如下,程序沒有crash。

那么問題來了!這些方法是在VC中實現的,如果我們想要給每個類都添加一個防止crash的方法呢?顯然這樣添加不是一個很好的選擇。

解決方案:

//創建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(@"在類:%@中 未實現該方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}

@end

控制臺打印如下,程序沒有crash。

因為在category中復寫了父類的方法,會出現下面的警告,



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



此處還有一點需要解釋的,就是在方法簽名中的Types:"v@:@",這些符號是什么意思呢?
其實這些符號就是返回值和方法參數對應的類型。可在Xcode中的開發者文檔中搜索Type Encodings就可看到符號對應的含義,此處不再列舉了。

消息轉發全流程

接收者在每一步中均有機會處理消息。步驟越往后,處理消息的代價越大。最好能在第一步處理完,這樣運行期系統可以把此方法緩存起來。如果這個類的實例稍后還會收到同名選擇子,則無須啟動消息轉發流程。
若想在第三步里把消息轉發給備援接收者,還不如把轉發操作提前到第二步。因為第三步只是修改了調用目標,這項改動放在第二步執行會更簡單,不然的話,還要創建并處理完整的NSIncocation。


demo放在GitHub上了,有需要的可以download下來查看.


可以利用消息轉發機制的三個步驟,選擇哪一步去改造比較合適呢?

這里我們選擇了第二步forwardingTargetForSelector。引用 《大白健康系統—iOS APP運行時Crash自動修復系統》 的分析:

  • resolveInstanceMethod 需要在類的本身上動態添加它本身不存在的方法,這些方法對于該類本身來說冗余的。
  • forwardInvocation 可以通過 NSInvocation 的形式將消息轉發給多個對象,但是其開銷較大,需要創建新的 NSInvocation 對象,并且 forwardInvocation 的函數經常被使用者調用,來做多層消息轉發選擇機制,不適合多次重寫。
  • forwardingTargetForSelector 可以將消息轉發給一個對象,開銷較小,并且被重寫的概率較低,適合重寫。

千里之行,始于足下。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379