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

1仑濒、方法method和selector(選擇子)有什么關(guān)系

在 Objective-C 中,selector偷遗,Method 和 implementation(IMP) 都是 Runtime 的組成部分墩瞳。在實(shí)際開(kāi)發(fā)中它們常常是可以相互轉(zhuǎn)換來(lái)處理消息的發(fā)送的。選擇子代表方法在 Runtime 期間的標(biāo)識(shí)符氏豌。為 SEL 類型喉酌,雖然 SEL 是 objc_selector 結(jié)構(gòu)體指針,但實(shí)際上它只是一個(gè) C 字符串泵喘。在類加載的時(shí)候泪电,編譯器會(huì)生成與方法相對(duì)應(yīng)的選擇子,并注冊(cè)到 Objective-C 的 Runtime 運(yùn)行系統(tǒng)涣旨。
得出結(jié)論:選擇子其實(shí)是方法的名稱歪架,不同類中方法名相同參數(shù)不同的倆個(gè)方法,他們的選擇子是相同的霹陡。

Method的結(jié)構(gòu)體

/// Method
struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
};
  • 方法名 method_name 類型為 SEL和蚪,前面提到過(guò)相同名字的方法即使在不同類中定義止状,它們的方法選擇器也相同。
  • 方法類型 method_types 是個(gè) char 指針攒霹,其實(shí)存儲(chǔ)著方法的參數(shù)類型和返回值類型怯疤,即是 Type Encoding 編碼。(即類型編碼)
  • method_imp 指向方法的實(shí)現(xiàn)催束,本質(zhì)上是一個(gè)函數(shù)的指針集峦,就是前面講到的 Implementation。

消息傳遞

在OC中抠刺,給對(duì)象發(fā)送消息的語(yǔ)法是:

id returnValue = [someObject messageName:parameter];

這里塔淤,someObject叫做接收者(receiver),messageName:叫做選擇子(selector),選擇子和參數(shù)合起來(lái)稱為“消息”速妖。編譯器看到此消息后高蜂,將其轉(zhuǎn)換為一條標(biāo)準(zhǔn)的C語(yǔ)言函數(shù)調(diào)用,所調(diào)用的函數(shù)乃是消息傳遞機(jī)制中的核心函數(shù)叫做objc_msgSend罕容,它的原型如下:

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

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

因此旅择,上述以O(shè)C形式展現(xiàn)出來(lái)的函數(shù)就會(huì)轉(zhuǎn)化成如下函數(shù):

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

可以看出惭笑,在調(diào)用方法時(shí),編譯器將它轉(zhuǎn)成了objc_msgSend消息發(fā)送了砌左,在Runtime的執(zhí)行過(guò)程如下

  • 1脖咐、Runtime先通過(guò)對(duì)象someobject找到isa指針铺敌,判斷isa指針是否為nil汇歹,為nil直接return。
  • 2偿凭、若不為空則通過(guò)isa指針找到當(dāng)前實(shí)例的類對(duì)象产弹,在類對(duì)象下查找緩存是否有messageName方法。
  • 3弯囊、若在類對(duì)象緩存中找到messageName方法痰哨,則直接調(diào)用IMP方法(本質(zhì)上是函數(shù)的指針)。
  • 4匾嘱、若在類對(duì)象緩存中沒(méi)找到messageName方法斤斧,則查找當(dāng)前類對(duì)象的方法列表methodlist,若找到方法則將其添加到類對(duì)象的緩存中霎烙。
  • 5撬讽、若在類對(duì)象方法列表中沒(méi)找到messageName方法蕊连,則繼續(xù)到當(dāng)前類的父類中以相同的方式查找(即類的緩存->類的方法列表)。
  • 6游昼、若在父類中找到messageName方法甘苍,則將IMP添加到類對(duì)象緩存中。
  • 7烘豌、若在父類中沒(méi)找到messageName方法载庭,則繼續(xù)查詢父類的父類,直到追溯到最上層NSObject廊佩。
  • 8囚聚、若還是沒(méi)有找到,則啟用動(dòng)態(tài)方法解析标锄、備用接收者靡挥、消息轉(zhuǎn)發(fā)三部曲,給程序最后一個(gè)機(jī)會(huì)
  • 9鸯绿、若還是沒(méi)找到跋破,則Runtime會(huì)拋出異常doesNotRecognizeSelector

綜上瓶蝴,方法的查詢流程基本就是查詢類對(duì)象中的緩存和方法列表->父類中的緩存和方法列表->父類的父類中的緩存和方法列表->...->NSObject中的緩存和方法列表->動(dòng)態(tài)方法解析->備用接收者->消息轉(zhuǎn)發(fā)毒返。

消息動(dòng)態(tài)解析

Objective-C 運(yùn)行時(shí)會(huì)調(diào)用 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機(jī)會(huì)提供一個(gè)函數(shù)實(shí)現(xiàn)舷手。前者在 對(duì)象方法未找到時(shí) 調(diào)用拧簸,后者在 類方法未找到時(shí) 調(diào)用。我們可以通過(guò)重寫(xiě)這兩個(gè)方法男窟,添加其他函數(shù)實(shí)現(xiàn)盆赤,并返回 YES, 那運(yùn)行時(shí)系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送的過(guò)程歉眷。

主要用的的方法如下:

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

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

測(cè)試代碼

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

person.m 文件中
  // 重寫(xiě) resolveInstanceMethod: 添加對(duì)象方法實(shí)現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(way)) {
        class_addMethod([self class], sel,class_getMethodImplementation([self class], @selector(method)), "123");//使用class_addMethod動(dòng)態(tài)添加方法method
    }
    return YES;
}
- (void)method{
    NSLog(@"進(jìn)入了消息動(dòng)態(tài)解析");
}
運(yùn)行了上述的測(cè)試代碼后淑际,我們會(huì)發(fā)現(xiàn)即便我們并沒(méi)有實(shí)現(xiàn)way方法,而且使用了performSelector去強(qiáng)行調(diào)用way方法扇住,但是我們的程序并沒(méi)有崩潰春缕,是因?yàn)樵诓檎伊祟惙椒ê退械母割惡筮€是沒(méi)有找到way方法,程序進(jìn)入了消息動(dòng)態(tài)解析艘蹋,然后我們使用了class_addMethod去動(dòng)態(tài)添加方法method锄贼,最后程序從調(diào)用

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

performSelector: withObject:是在iOS中的一種方法調(diào)用方式。他可以向一個(gè)對(duì)象傳遞任何消息女阀,而不需要在編譯的時(shí)候聲明這些方法宅荤。所以這也是runtime的一種應(yīng)用方式米间。

所以performSelector和直接調(diào)用方法的區(qū)別就在與runtime。直接調(diào)用編譯是會(huì)自動(dòng)校驗(yàn)膘侮。如果方法不存在屈糊,那么直接調(diào)用 在編譯時(shí)候就能夠發(fā)現(xiàn),編譯器會(huì)直接報(bào)錯(cuò)琼了。
但是使用performSelector的話一定是在運(yùn)行時(shí)候才能發(fā)現(xiàn)逻锐,如果此方法不存在就會(huì)崩潰。所以一般使用performSelector的時(shí)候雕薪,一般都會(huì)使用- (BOOL)respondsToSelector:(SEL)aSelector;來(lái)在運(yùn)行時(shí)判斷對(duì)象是否響應(yīng)此方法昧诱。

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

如果上一步中 +resolveInstanceMethod: 或者 +resolveClassMethod: 沒(méi)有添加其他函數(shù)實(shí)現(xiàn),運(yùn)行時(shí)就會(huì)進(jìn)行下一步:消息接受者重定向所袁。

如果當(dāng)前對(duì)象實(shí)現(xiàn)了 -forwardingTargetForSelector: 或者 +forwardingTargetForSelector: 方法盏档,Runtime 就會(huì)調(diào)用這個(gè)方法,允許我們將消息的接受者轉(zhuǎn)發(fā)給其他對(duì)象燥爷。

其中用到的方法蜈亩。

// 重定向類方法的消息接收者,返回一個(gè)類或?qū)嵗龑?duì)象
+ (id)forwardingTargetForSelector:(SEL)aSelector;
// 重定向方法的消息接收者前翎,返回一個(gè)類或?qū)嵗龑?duì)象
- (id)forwardingTargetForSelector:(SEL)aSelector;

注意:

  1. 類方法和對(duì)象方法消息轉(zhuǎn)發(fā)第二步調(diào)用的方法不一樣稚配,前者是+forwardingTargetForSelector: 方法,后者是 -forwardingTargetForSelector: 方法港华。
  2. 這里+resolveInstanceMethod: 或者 +resolveClassMethod:無(wú)論是返回 YES道川,還是返回 NO,只要其中沒(méi)有添加其他函數(shù)實(shí)現(xiàn)立宜,運(yùn)行時(shí)都會(huì)進(jìn)行下一步冒萄。

測(cè)試代碼

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

可以看到橙数,雖然當(dāng)前 person 沒(méi)有實(shí)現(xiàn) fun 方法尊流,+resolveInstanceMethod: 也沒(méi)有添加其他函數(shù)實(shí)現(xiàn)。但是我們通過(guò) forwardingTargetForSelector 把當(dāng)前 person 的方法轉(zhuǎn)發(fā)給了 friends 對(duì)象去執(zhí)行了商模。打印結(jié)果也證明我們成功實(shí)現(xiàn)了轉(zhuǎn)發(fā)奠旺。

我們通過(guò) forwardingTargetForSelector 可以修改消息的接收者蜘澜,該方法返回參數(shù)是一個(gè)對(duì)象施流,如果這個(gè)對(duì)象是不是 nil,也不是 self鄙信,系統(tǒng)會(huì)將運(yùn)行的消息轉(zhuǎn)發(fā)給這個(gè)對(duì)象執(zhí)行瞪醋。否則,繼續(xù)進(jìn)行下一步:消息重定向流程装诡。

消息重定向

如果經(jīng)過(guò)消息動(dòng)態(tài)解析银受、消息接受者重定向践盼,Runtime 系統(tǒng)還是找不到相應(yīng)的方法實(shí)現(xiàn)而無(wú)法響應(yīng)消息,Runtime 系統(tǒng)會(huì)利用 -methodSignatureForSelector: 或者 +methodSignatureForSelector: 方法獲取函數(shù)的參數(shù)和返回值類型宾巍。

  • 如果 methodSignatureForSelector: 返回了一個(gè) NSMethodSignature 對(duì)象(函數(shù)簽名)咕幻,Runtime 系統(tǒng)就會(huì)創(chuàng)建一個(gè) NSInvocation 對(duì)象,并通過(guò) forwardInvocation: 消息通知當(dāng)前對(duì)象顶霞,給予此次消息發(fā)送最后一次尋找 IMP 的機(jī)會(huì)肄程。
  • 如果 methodSignatureForSelector: 返回 nil。則 Runtime 系統(tǒng)會(huì)發(fā)出 doesNotRecognizeSelector: 消息选浑,程序也就崩潰了蓝厌。

所以我們可以在 forwardInvocation: 方法中對(duì)消息進(jìn)行轉(zhuǎn)發(fā)。

注意:類方法和對(duì)象方法消息轉(zhuǎn)發(fā)第三步調(diào)用的方法同樣不一樣古徒。
類方法調(diào)用的是:

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

對(duì)象方法調(diào)用的是:

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

用到的方法:

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

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

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

// 對(duì)象方法消息重定向
- (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 對(duì)象方法是否可以響應(yīng) sel
        [anInvocation invokeWithTarget:f];  // 若可以響應(yīng),則將消息轉(zhuǎn)發(fā)給其他對(duì)象處理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然無(wú)法響應(yīng)疹吃,則報(bào)錯(cuò):找不到響應(yīng)方法
    }
}

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

區(qū)別就在于 -forwardingTargetForSelector: 只能將消息轉(zhuǎn)發(fā)給一個(gè)對(duì)象互墓。而 -forwardInvocation: 可以將消息轉(zhuǎn)發(fā)給多個(gè)對(duì)象必尼。

以上就是 Runtime 消息轉(zhuǎn)發(fā)的整個(gè)流程。

消息發(fā)送以及轉(zhuǎn)發(fā)機(jī)制總結(jié)

調(diào)用 [receiver selector]; 后篡撵,進(jìn)行的流程:

  1. 編譯階段:

    [receiver selector]; 方法被編譯器轉(zhuǎn)換為:

    1. objc_msgSend(receiver判莉,selector) (不帶參數(shù))
    2. objc_msgSend(recevier,selector育谬,org1券盅,org2,…)(帶參數(shù))
  2. 運(yùn)行時(shí)階段:消息接受者

    recevier尋找對(duì)應(yīng)的 selector

    1. 通過(guò) recevierisa 指針 找到 recevierclass(類)膛檀;
    2. Class(類)cache(方法緩存) 的散列表中尋找對(duì)應(yīng)的 IMP(方法實(shí)現(xiàn))锰镀;
    3. 如果在 cache(方法緩存) 中沒(méi)有找到對(duì)應(yīng)的 IMP(方法實(shí)現(xiàn)) 的話,就繼續(xù)在 Class(類)method list(方法列表) 中找對(duì)應(yīng)的 selector咖刃,如果找到泳炉,填充到 cache(方法緩存) 中,并返回 selector嚎杨;
    4. 如果在 class(類) 中沒(méi)有找到這個(gè) selector花鹅,就繼續(xù)在它的 superclass(父類)中尋找;
    5. 一旦找到對(duì)應(yīng)的 selector枫浙,直接執(zhí)行 recevier 對(duì)應(yīng) selector 方法實(shí)現(xiàn)的 IMP(方法實(shí)現(xiàn))刨肃。
    6. 若找不到對(duì)應(yīng)的 selector古拴,Runtime 系統(tǒng)進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制。
  3. 運(yùn)行時(shí)消息轉(zhuǎn)發(fā)階段:

    1. 動(dòng)態(tài)解析:通過(guò)重寫(xiě) +resolveInstanceMethod: 或者 +resolveClassMethod:方法真友,利用 class_addMethod方法添加其他函數(shù)實(shí)現(xiàn)黄痪;
    2. 消息接受者重定向:如果上一步添加其他函數(shù)實(shí)現(xiàn),可在當(dāng)前對(duì)象中利用 forwardingTargetForSelector: 方法將消息的接受者轉(zhuǎn)發(fā)給其他對(duì)象盔然;
    3. 消息重定向:如果上一步?jīng)]有返回值為 nil满力,則利用 methodSignatureForSelector:方法獲取函數(shù)的參數(shù)和返回值類型。
      1. 如果 methodSignatureForSelector: 返回了一個(gè) NSMethodSignature 對(duì)象(函數(shù)簽名)轻纪,Runtime 系統(tǒng)就會(huì)創(chuàng)建一個(gè) NSInvocation 對(duì)象油额,并通過(guò) forwardInvocation: 消息通知當(dāng)前對(duì)象,給予此次消息發(fā)送最后一次尋找 IMP 的機(jī)會(huì)刻帚。
      2. 如果 methodSignatureForSelector: 返回 nil潦嘶。則 Runtime 系統(tǒng)會(huì)發(fā)出 doesNotRecognizeSelector: 消息,程序也就崩潰了崇众。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末沫勿,一起剝皮案震驚了整個(gè)濱河市闯传,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖浪感,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件背亥,死亡現(xiàn)場(chǎng)離奇詭異览闰,居然都是意外死亡吨娜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)赦抖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)舱卡,“玉大人,你說(shuō)我怎么就攤上這事队萤÷肿叮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵要尔,是天一觀的道長(zhǎng)舍杜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)赵辕,這世上最難降的妖魔是什么既绩? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮匆帚,結(jié)果婚禮上熬词,老公的妹妹穿的比我還像新娘。我一直安慰自己吸重,他們只是感情好互拾,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著嚎幸,像睡著了一般颜矿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嫉晶,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天骑疆,我揣著相機(jī)與錄音,去河邊找鬼替废。 笑死箍铭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的椎镣。 我是一名探鬼主播诈火,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼状答!你這毒婦竟也來(lái)了冷守?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤惊科,失蹤者是張志新(化名)和其女友劉穎拍摇,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體馆截,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡充活,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年蜡娶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翎蹈。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖荤堪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澄阳,我是刑警寧澤拥知,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布碎赢,位于F島的核電站低剔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜襟齿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一姻锁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猜欺,春花似錦位隶、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至赋荆,卻和暖如春笋妥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窄潭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工春宣, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狈孔。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓信认,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親均抽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嫁赏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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