IOS 消息傳遞與消息轉發(fā)

1、方法method和selector(選擇子)有什么關系

在 Objective-C 中,selector,Method 和 implementation(IMP) 都是 Runtime 的組成部分。在實際開發(fā)中它們常常是可以相互轉換來處理消息的發(fā)送的。選擇子代表方法在 Runtime 期間的標識符。為 SEL 類型,雖然 SEL 是 objc_selector 結構體指針,但實際上它只是一個 C 字符串。在類加載的時候,編譯器會生成與方法相對應的選擇子,并注冊到 Objective-C 的 Runtime 運行系統(tǒng)。
得出結論:選擇子其實是方法的名稱,不同類中方法名相同參數不同的倆個方法,他們的選擇子是相同的。

Method的結構體

/// Method
struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
};
  • 方法名 method_name 類型為 SEL,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。
  • 方法類型 method_types 是個 char 指針,其實存儲著方法的參數類型和返回值類型,即是 Type Encoding 編碼。(即類型編碼)
  • method_imp 指向方法的實現(xiàn),本質上是一個函數的指針,就是前面講到的 Implementation。

消息傳遞

在OC中,給對象發(fā)送消息的語法是:

id returnValue = [someObject messageName:parameter];

這里,someObject叫做接收者(receiver),messageName:叫做選擇子(selector),選擇子和參數合起來稱為“消息”。編譯器看到此消息后,將其轉換為一條標準的C語言函數調用,所調用的函數乃是消息傳遞機制中的核心函數叫做objc_msgSend,它的原型如下:

void objc_msgSend(id self, SEL cmd, ...)

第一個參數代表接收者,第二個參數代表選擇子,后續(xù)參數就是消息中的那些參數,數量是可變的·,所以這個函數就是參數個數可變的函數。

因此,上述以OC形式展現(xiàn)出來的函數就會轉化成如下函數:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

可以看出,在調用方法時,編譯器將它轉成了objc_msgSend消息發(fā)送了,在Runtime的執(zhí)行過程如下

  • 1、Runtime先通過對象someobject找到isa指針,判斷isa指針是否為nil,為nil直接return。
  • 2、若不為空則通過isa指針找到當前實例的類對象,在類對象下查找緩存是否有messageName方法。
  • 3、若在類對象緩存中找到messageName方法,則直接調用IMP方法(本質上是函數的指針)。
  • 4、若在類對象緩存中沒找到messageName方法,則查找當前類對象的方法列表methodlist,若找到方法則將其添加到類對象的緩存中。
  • 5、若在類對象方法列表中沒找到messageName方法,則繼續(xù)到當前類的父類中以相同的方式查找(即類的緩存->類的方法列表)。
  • 6、若在父類中找到messageName方法,則將IMP添加到類對象緩存中。
  • 7、若在父類中沒找到messageName方法,則繼續(xù)查詢父類的父類,直到追溯到最上層NSObject
  • 8、若還是沒有找到,則啟用動態(tài)方法解析、備用接收者、消息轉發(fā)三部曲,給程序最后一個機會
  • 9、若還是沒找到,則Runtime會拋出異常doesNotRecognizeSelector

綜上,方法的查詢流程基本就是查詢類對象中的緩存和方法列表->父類中的緩存和方法列表->父類的父類中的緩存和方法列表->...->NSObject中的緩存和方法列表->動態(tài)方法解析->備用接收者->消息轉發(fā)

消息動態(tài)解析

Objective-C 運行時會調用 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機會提供一個函數實現(xiàn)。前者在 對象方法未找到時 調用,后者在 類方法未找到時 調用。我們可以通過重寫這兩個方法,添加其他函數實現(xiàn),并返回 YES, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程。

主要用的的方法如下:

// 類方法未找到時調起,可以在此添加方法實現(xiàn)
+ (BOOL)resolveClassMethod:(SEL)sel;
// 對象方法未找到時調起,可以在此添加方法實現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel;

/** 
 * class_addMethod    向具有給定名稱和實現(xiàn)的類中添加新方法
 * @param cls         被添加方法的類
 * @param name        selector 方法名
 * @param imp         實現(xiàn)方法的函數指針
 * @param types imp   指向函數的返回值與參數類型
 * @return            如果添加方法成功返回 YES,否則返回 NO
 */
BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                const char * _Nullable types);

測試代碼

main.m 文件中
  int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    Person *xiaoming = [[Person alloc]init];
    [xiaoming performSelector:@selector(way)];
    return 0;
    
}

person.m 文件中
  // 重寫 resolveInstanceMethod: 添加對象方法實現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(way)) {
        class_addMethod([self class], sel,class_getMethodImplementation([self class], @selector(method)), "123");//使用class_addMethod動態(tài)添加方法method
    }
    return YES;
}
- (void)method{
    NSLog(@"進入了消息動態(tài)解析");
}
運行了上述的測試代碼后,我們會發(fā)現(xiàn)即便我們并沒有實現(xiàn)way方法,而且使用了performSelector去強行調用way方法,但是我們的程序并沒有崩潰,是因為在查找了類方法和所有的父類后還是沒有找到way方法,程序進入了消息動態(tài)解析,然后我們使用了class_addMethod去動態(tài)添加方法method,最后程序從調用

performSelector和直接調用方法的區(qū)別

performSelector: withObject:是在iOS中的一種方法調用方式。他可以向一個對象傳遞任何消息,而不需要在編譯的時候聲明這些方法。所以這也是runtime的一種應用方式。

所以performSelector和直接調用方法的區(qū)別就在與runtime。直接調用編譯是會自動校驗。如果方法不存在,那么直接調用 在編譯時候就能夠發(fā)現(xiàn),編譯器會直接報錯。
但是使用performSelector的話一定是在運行時候才能發(fā)現(xiàn),如果此方法不存在就會崩潰。所以一般使用performSelector的時候,一般都會使用- (BOOL)respondsToSelector:(SEL)aSelector;來在運行時判斷對象是否響應此方法。

消息接受者重定向(備用接受者)

如果上一步中 +resolveInstanceMethod: 或者 +resolveClassMethod: 沒有添加其他函數實現(xiàn),運行時就會進行下一步:消息接受者重定向。

如果當前對象實現(xiàn)了 -forwardingTargetForSelector: 或者 +forwardingTargetForSelector: 方法,Runtime 就會調用這個方法,允許我們將消息的接受者轉發(fā)給其他對象。

其中用到的方法。

// 重定向類方法的消息接收者,返回一個類或實例對象
+ (id)forwardingTargetForSelector:(SEL)aSelector;
// 重定向方法的消息接收者,返回一個類或實例對象
- (id)forwardingTargetForSelector:(SEL)aSelector;

注意:

  1. 類方法和對象方法消息轉發(fā)第二步調用的方法不一樣,前者是+forwardingTargetForSelector: 方法,后者是 -forwardingTargetForSelector: 方法。
  2. 這里+resolveInstanceMethod: 或者 +resolveClassMethod:無論是返回 YES,還是返回 NO,只要其中沒有添加其他函數實現(xiàn),運行時都會進行下一步。

測試代碼

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(way)) {
        Friends *friends = [[Friends alloc]init];
        return friends;//返回friends對象,讓friends對象接受這個消息
    }
    return [super forwardingTargetForSelector:aSelector];
}

可以看到,雖然當前 person 沒有實現(xiàn) fun 方法,+resolveInstanceMethod: 也沒有添加其他函數實現(xiàn)。但是我們通過 forwardingTargetForSelector 把當前 person 的方法轉發(fā)給了 friends 對象去執(zhí)行了。打印結果也證明我們成功實現(xiàn)了轉發(fā)。

我們通過 forwardingTargetForSelector 可以修改消息的接收者,該方法返回參數是一個對象,如果這個對象是不是 nil,也不是 self,系統(tǒng)會將運行的消息轉發(fā)給這個對象執(zhí)行。否則,繼續(xù)進行下一步:消息重定向流程。

消息重定向

如果經過消息動態(tài)解析、消息接受者重定向,Runtime 系統(tǒng)還是找不到相應的方法實現(xiàn)而無法響應消息,Runtime 系統(tǒng)會利用 -methodSignatureForSelector: 或者 +methodSignatureForSelector: 方法獲取函數的參數和返回值類型。

  • 如果 methodSignatureForSelector: 返回了一個 NSMethodSignature 對象(函數簽名),Runtime 系統(tǒng)就會創(chuàng)建一個 NSInvocation 對象,并通過 forwardInvocation: 消息通知當前對象,給予此次消息發(fā)送最后一次尋找 IMP 的機會。
  • 如果 methodSignatureForSelector: 返回 nil。則 Runtime 系統(tǒng)會發(fā)出 doesNotRecognizeSelector: 消息,程序也就崩潰了。

所以我們可以在 forwardInvocation: 方法中對消息進行轉發(fā)。

注意:類方法和對象方法消息轉發(fā)第三步調用的方法同樣不一樣。
類方法調用的是:

  1. + methodSignatureForSelector:
  2. + forwardInvocation:
  3. + doesNotRecognizeSelector:

對象方法調用的是:

  1. - methodSignatureForSelector:
  2. - forwardInvocation:
  3. - doesNotRecognizeSelector:

用到的方法:

// 獲取類方法函數的參數和返回值類型,返回簽名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 類方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

// 獲取對象方法函數的參數和返回值類型,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 對象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(way)) {
        return [NSMethodSignature methodSignatureForSelector:@selector(way)];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    Friends *f = [[Friends alloc] init];
    if([f respondsToSelector:sel]) {   // 判斷 Person 對象方法是否可以響應 sel
        [anInvocation invokeWithTarget:f];  // 若可以響應,則將消息轉發(fā)給其他對象處理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然無法響應,則報錯:找不到響應方法
    }
}

既然 -forwardingTargetForSelector:-forwardInvocation: 都可以將消息轉發(fā)給其他對象處理,那么兩者的區(qū)別在哪?

區(qū)別就在于 -forwardingTargetForSelector: 只能將消息轉發(fā)給一個對象。而 -forwardInvocation: 可以將消息轉發(fā)給多個對象。

以上就是 Runtime 消息轉發(fā)的整個流程。

消息發(fā)送以及轉發(fā)機制總結

調用 [receiver selector]; 后,進行的流程:

  1. 編譯階段:

    [receiver selector]; 方法被編譯器轉換為:

    1. objc_msgSend(receiver,selector) (不帶參數)
    2. objc_msgSend(recevier,selector,org1,org2,…)(帶參數)
  2. 運行時階段:消息接受者

    recevier尋找對應的 selector

    1. 通過 recevierisa 指針 找到 recevierclass(類)
    2. Class(類)cache(方法緩存) 的散列表中尋找對應的 IMP(方法實現(xiàn))
    3. 如果在 cache(方法緩存) 中沒有找到對應的 IMP(方法實現(xiàn)) 的話,就繼續(xù)在 Class(類)method list(方法列表) 中找對應的 selector,如果找到,填充到 cache(方法緩存) 中,并返回 selector
    4. 如果在 class(類) 中沒有找到這個 selector,就繼續(xù)在它的 superclass(父類)中尋找;
    5. 一旦找到對應的 selector,直接執(zhí)行 recevier 對應 selector 方法實現(xiàn)的 IMP(方法實現(xiàn))
    6. 若找不到對應的 selector,Runtime 系統(tǒng)進入消息轉發(fā)機制。
  3. 運行時消息轉發(fā)階段:

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

推薦閱讀更多精彩內容