runtime-消息傳遞與轉發機制

參考文章:
繼承自NSObject的不常用又很有用的函數【重點推薦】
Objective-C Runtime 1小時入門教程【重點推薦】
類的本質-類對象
運行時消息傳遞與轉發機制
深入淺出理解消息的傳遞和轉發機制
消息轉發機制原理和實際用途

image.png
image.png

一、消息傳遞過程

objc_msgSend()函數會依據接收者(調用方法的對象)的類型和選擇子(方法名)來調用適當的方法。
1、接收者會根據isa指針找到接收者自己所屬的類,然后在該類的緩存中查找對應的IMP,如果找到了,則根據IMP指針跳轉到方法的實現代碼,調用這個方法的實現;如果沒有緩存則初始化緩存,進入步驟2

方法緩存:
發現調用一個方法并不像我們想的那么簡單,更不像我們寫的那么簡單,一個方法的執行其實底層需要很多步驟。
正因如此,objc_msgSend()會將調用過且匹配到的方法緩存在”快速映射表(fast map)“中,快速映射表就是方法的緩存表。每個類都有這樣一個緩存。
所以,即便子類實例從父類的方法列表中取過了某個對象方法,那么子類的方法緩存表中也會緩存父類的這個方法,下次調用這個方法,會優先去當前類(對象所屬的類)的方法緩存表中查找這個方法,這樣的好處是顯而易見的,減少了漫長的方法查找過程,使得方法的調用更快。
同樣,如果父類實例對象調用了同樣的方法,也會在父類的方法緩存表中緩存這個方法。
同理,如果用一個子類對象調用某個類方法,也會在子類的metaclass里緩存一份。而當用一個父類對象去調用那個類方法的時候,也會在父類的metaclass里緩存一份。

2、在所屬類的”方法列表“(method list)中從上向下遍歷。如果能找到與選擇子名稱相符的方法,就根據IMP指針跳轉到方法的實現代碼,調用這個方法的實現。
3、如果找不到與選擇子名稱相符的方法,接收者會根據所屬類的superClass指針,沿著類的繼承體系繼續向上查找(向父類查找),如果能找到與名稱相符的方法,就根據IMP指針跳轉到方法的實現代碼,調用這個方法的實現。
4、如果在繼承體系中還是找不到與選擇子相符的方法,此時就會執行”消息轉發(message forwarding)“操作。

二、消息轉發過程

image.png

Q:說一下你理解的消息轉發機制?

解說如下:

先會調用objc_msgSend方法,根據消息接收者對象的isa指針,找到接收者對象所屬的類Class,首先在Class的緩存中查找IMP,沒有緩存則初始化緩存。如果沒有找到,則通過Class的superClass指針向父類的Class查找。如果一直查找到根類【即在繼承體系中查找】仍舊沒有實現,則執行消息轉發。

當遇到一個方法調用,編譯器會生成一個objc_msgSend的調用,有4種:
objc_msgSend:其他的消息會使用
objc_msgSend_stret:
objc_msgSendSuper: 發送給父類的message會使用
objc_msgSendSuper_stret:
如果方法的返回值是一個結構體(structures),那么就會使用objc_msgSendSuper_stret或者objc_msgSend_stret。

1、動態方法解析:

調用resolveInstanceMethod:方法。允許用戶在此時為該Class動態添加實現。如果有實現了,則調用并返回YES,重新開始objc_msgSend流程。這次對象會響應這個選擇器,一般是因為它已經調用過了class_addMethod。如果仍沒有實現,繼續下面的動作。

在當前類中重寫此方法:
void gotoSchool(id self,SEL _cmd,id value) {
    printf("go to school");
}
//第一步:對象在收到無法解讀的消息后,首先將調用所屬類的該方法。
//這個函數在運行時(runtime),沒有找到SEL的IML時就會執行。
//這個函數是給類利用class_addMethod添加函數的機會。
//根據文檔,如果實現了添加函數代碼則返回YES,未實現返回NO。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"gotoschool"]) {
        class_addMethod(self, sel, (IMP)gotoSchool, "@@:");
    }
    return [super resolveInstanceMethod:sel];
}

如果運行期系統已經執行完了動態方法解析,那么消息接受者自己就無法再以動態新增方法的形式來響應包含該未知選擇子的消息了,此時就進入了第二階段——完整的消息轉發。運行期系統會請求消息接受者以其他手段來處理與消息相關的方法調用。

2、備援接收者

調用forwardingTargetForSelector:方法,嘗試找到一個能響應該消息的對象。如果獲取到,則直接把消息轉發給它,返回非nil對象。否則返回nil,繼續下面的動作。注意這里不要返回self,否則會形成死循環。

//第三步:備援接收者,讓其他對象進行處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selectorString = NSStringFromSelector(aSelector);
    if ([selectorString isEqualToString:@"gotoschool"]) {
        return self.student;
    }
    return nil;
}

3、完整的消息轉發

3.1 調用methodSignatureForSelector:方法,嘗試獲得一個方法簽名。如果獲取不到,則直接調用doesNotRecognizeSelector拋出異常。如果能獲取,則返回非nil;傳給一個NSInvocation并傳給forwardInvocation:。

對一個你的對象不識別的消息進行響應,你必須重寫methodSignatureForSelector:方法,該方法返回一個NSMethodSIgnature對象,該對象包含了給定選擇器所標識方法的描述(如:方法名SEL、方法參數、方法返回值、接收者等信息)。主要包含返回值的信息和參數信息。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"@@:"];
    return sign;
}

3.2 調用forwardInvocation:方法,將上一步獲取到的方法簽名包裝成Invocation傳入,如何處理就在這里面了,并返回非nil。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@ can't handle by People",NSStringFromSelector([anInvocation selector]));
}

forwardInvocation:真正執行從methodSignatureForSelector:返回的NSMethodSignature。在forwardInvocation:函數里可以將NSInvocation多次轉發到多個對象中,這也是這種方式靈活的地方。(forwardingTargetForSelector只能以Selector的形式轉向一個對象)

// 第一步:我們不動態添加方法,返回NO,進入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO;
}

// 第二部:我們不指定備選對象響應aSelector,進入第三步;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return nil;
}

// 第三步:返回方法選擇器,然后進入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector];
}

// 第四部:這步我們修改調用方法
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation setSelector:@selector(dance)];
    // 這還要指定是哪個對象的方法
    [anInvocation invokeWithTarget:self];
}

// 若forwardInvocation沒有實現,則會調用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
    NSLog(@"消息無法處理:%@", NSStringFromSelector(aSelector));
}

- (void)dance
{
    NSLog(@"跳舞!!!come on!");
}

消息派發系統觸發消息前,會以某種方式改變消息內容,包括但不限于額外追加一個參數、改變選擇子等。
實現此方法時,如果發現調用操作不應該由本類處理,則需要沿著繼承體系,調用父類的同名方法,這樣一來,繼承體系中的每個類都有機會處理這個調用請求,直至rootClass,也就是NSObject類。
如果最后調用了NSObject的類方法,那么該方法還會繼而調用”doesNotRecognizeSelector:“以拋出異常,此異常表明選擇子最終也未能得到處理。消息轉發到此結束。

3.3調用doesNotRecognizeSelector:,默認的實現是拋出異常。如果第三步沒能獲得一個方法簽名,執行該步驟 。

擴展

1、對象調用method代碼示例

iOS 使用NSMethodSignature和 NSInvocation進行 method 或 block的調用
一個實例對象可以通過三種方式調用其方法。

- (void)test{
    
//type1
    [self printStr1:@"hello world 1"];
    
//type2
    [self performSelector:@selector(printStr1:) withObject:@"hello world 2"];
    
//type3
    //獲取方法簽名
    NSMethodSignature *sigOfPrintStr = [self methodSignatureForSelector:@selector(printStr1:)];
    
    //獲取方法簽名對應的invocation
    NSInvocation *invocationOfPrintStr = [NSInvocation invocationWithMethodSignature:sigOfPrintStr];
    
    /**
    設置消息接受者,與[invocationOfPrintStr setArgument:(__bridge void * _Nonnull)(self) atIndex:0]等價
    */
    [invocationOfPrintStr setTarget:self];
    
    /**設置要執行的selector。與[invocationOfPrintStr setArgument:@selector(printStr1:) atIndex:1] 等價*/
    [invocationOfPrintStr setSelector:@selector(printStr1:)];
    
    //設置參數 
    NSString *str = @"hello world 3";
    [invocationOfPrintStr setArgument:&str atIndex:2];
    
    //開始執行
    [invocationOfPrintStr invoke];
}

- (void)printStr1:(NSString*)str{
    NSLog(@"printStr1  %@",str);
}

2、ObjcTypes

它是一個是字符串數組,該數組包含了方法的類型編碼。
如:"v@:@"。
那究竟是如何得來該字符串呢?其實我們有兩種方式:

  1. 直接查表。在Type Encodings里面列出了對應關系。
  2. 使用 @encode()計算。( NSLog(@"%s",@encode(BOOL))的結果為B )

在OC中,每一種數據類型可以通過一個字符編碼來表示(Objective-C type encodings)。例如字符‘@’代表一個object, 'i'代表int。 那么,由這些字符組成的字符數組就可以表示方法類型了。
舉個例子:printStr1:對應的ObjCTypes 為 v@:@。

// 第三步:返回方法選擇器,然后進入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector];
}

’v‘ : void類型,第一個字符代表返回值類型
’@‘ : 一個id類型的對象,第一個參數類型
’:‘ : 對應SEL,第二個參數類型
’@‘ : 一個id類型的對象,第三個參數類型,也就是- (void)printStr1:(NSString*)str中的str。

消息發送會被轉換成objc _ msgSend(id reciever,SEL sel,prarams1,params2,....)。所以上面的:

- (void)printStr1:(NSString*)str{
    NSLog(@"printStr1  %@",str);
}

[zhagnsan printStr1:lisi]
//方法會被轉換成
void objc_msgSend(zhangsan,@selector(printStr1:),lisi);   //包含兩個隱藏參數

這里的 “v@:@”就代表:

"v":代表返回值void
"@":代表一個對象,這里指代的id類型zhangsan,也就是消息的receiver
":":代表SEL
"@":代表參數lisi

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

推薦閱讀更多精彩內容