Runtime--消息轉(zhuǎn)發(fā)機(jī)制

消息轉(zhuǎn)發(fā)機(jī)制,顧名思義,就是消息被轉(zhuǎn)發(fā)了,但是這個(gè)轉(zhuǎn)發(fā)呢,有一個(gè)制度需要遵循,廢話少說(shuō),直接進(jìn)入正題。

首先我們了解一下消息傳遞的形式

object-c是一門動(dòng)態(tài)語(yǔ)言,我們?cè)趏bject-c中每次調(diào)起一個(gè)方法就是想這個(gè)對(duì)象發(fā)送一條消息,也就是進(jìn)行一次消息傳遞,比方說(shuō):

[self sendMsg:@"參數(shù)"];

此時(shí),在編譯時(shí)調(diào)起object-c函數(shù)就會(huì)轉(zhuǎn)化為調(diào)起c函數(shù),在解析這條消息之前,我們先來(lái)看一下消息傳遞機(jī)制中所調(diào)用的核心函數(shù):

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

解釋一下各個(gè)參數(shù)代表的意思,第一個(gè)參數(shù):消息接收者;第二個(gè)參數(shù):選擇子,它最少需要帶兩個(gè)參數(shù),后面的參數(shù)可以是消息傳遞的參數(shù),數(shù)量不限。

上面的函數(shù)調(diào)起時(shí)會(huì)轉(zhuǎn)化為如下形式發(fā)送消息:

object_msgSend(id self, SEL sendMsg,@"參數(shù)")

括號(hào)中三個(gè)參數(shù)的意義:
1、 第一個(gè)參數(shù)是接收者,指的是調(diào)起參數(shù)的類或者對(duì)象;
2、 第二個(gè)參數(shù)是選擇子,指的是類或者對(duì)象調(diào)用的方法;
3、 第三個(gè)參數(shù)是方法中需要傳遞的參數(shù),可以為一個(gè),也可以為多個(gè)。

了解了傳遞的形式,我們?cè)賮?lái)看一下消息轉(zhuǎn)發(fā)的機(jī)制

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

消息轉(zhuǎn)發(fā)機(jī)制有三種方式

1、動(dòng)態(tài)方法解析階段
(1)+ (BOOL)resolveClassMethod:(SEL)sel
(2)+ (BOOL)resolveInstanceMethod:(SEL)sel
(1)是類方法解析,(2)是對(duì)象方法解析。如果動(dòng)態(tài)方法解析找到了這個(gè)方法,就會(huì)返回yes,如果沒有找到這個(gè)方法,就會(huì)進(jìn)入第二個(gè)階段

2、備援接收階段
-(id)forwardingTargetForSelector:(SEL)aSelector
在這個(gè)方法中我們可以把a(bǔ)Selector這個(gè)方法轉(zhuǎn)發(fā)給其他對(duì)象來(lái)調(diào)用,如果沒有備援接收者,那么,對(duì)于消息的處理就會(huì)進(jìn)入第三個(gè)階段

3、完整消息轉(zhuǎn)發(fā)階段
-(void)forwardInvocation:(NSInvocation *)anInvocation
這個(gè)方法主要是轉(zhuǎn)發(fā)給多個(gè)處理對(duì)象,和第二個(gè)階段類似,只是第二個(gè)階段是轉(zhuǎn)發(fā)個(gè)一個(gè)對(duì)象,而第三個(gè)階段是轉(zhuǎn)發(fā)個(gè)多個(gè)對(duì)象。


介紹完了消息轉(zhuǎn)發(fā)的三種方式,現(xiàn)在我們看一下具體的實(shí)現(xiàn)。

一、動(dòng)態(tài)方法解析階段的實(shí)現(xiàn)

首先,我們先新建一個(gè)工程,具體名字根據(jù)個(gè)人喜好來(lái)定,我起的名字是RuntimeWithMsgSend。
我在工程中新建了三個(gè)類,都繼承自NSObject,分別為Cat類,Dog類,Rabbit類,然后我又新建了一個(gè)繼承自UIViewController類的AnimalsViewController類,用來(lái)實(shí)現(xiàn)Cat類、Dog類和Rabbit類的調(diào)用。
我先在Cat.h文件中聲明一個(gè)sayHello的對(duì)象方法,但是沒有在Cat.m文件中并沒有實(shí)現(xiàn)這個(gè)方法,我們可以看到在Cat.m文件中有一個(gè)警告,如下圖:

聲明的方法未實(shí)現(xiàn)的警告

我們?cè)贏nimalsViewController中使用Cat類的對(duì)象去調(diào)用sayHello方法,如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    Cat *cat = [[Cat alloc] init];
    [cat sayHello];
    
}

運(yùn)行結(jié)果如下:

運(yùn)行結(jié)果

程序crash了,說(shuō)的是找不到這個(gè)選擇器,當(dāng)然,我們學(xué)習(xí)寫程序并不是為了制造bug的,但是,如果出現(xiàn)了這種bug我們應(yīng)該怎么辦呢?我們可以使用runtime來(lái)防止程序crash,這是它的作用之一。當(dāng)我們cat這個(gè)對(duì)象調(diào)用sayHello方法找不到的時(shí)候,就會(huì)進(jìn)入動(dòng)態(tài)方法解析階段,我們需要重寫+ (BOOL)resolveInstanceMethod:(SEL)sel這個(gè)方法,只要在這個(gè)方法中做一些操作,對(duì)傳過(guò)來(lái)的選擇子進(jìn)行處理,就有可能避免程序crash,在我們視線此方法之前,我們需要倒入頭文件:#import <objc/runtime.h>,方法的具體實(shí)現(xiàn)如下:

void say(id self, SEL _cmd){
    NSLog(@"say-----%@",self);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if ([NSStringFromSelector(sel) isEqualToString:@"sayHello"]) {
//        class_addMethod([self superclass], sel, (IMP)say, "v@:");
        class_addMethod([self class], sel, (IMP)say, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

其中,if判斷語(yǔ)句中是判斷當(dāng)前方法是否是你要?jiǎng)討B(tài)添加的方法,而class_addMethod(Class _Nullable __unsafe_unretained cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)是runtime中的方法,它的作用是向該類中的實(shí)例對(duì)象添加相應(yīng)的方法實(shí)現(xiàn),四個(gè)參數(shù)分別代表

  • cls 想要添加的類對(duì)象
  • name 添加后的方法名
  • imp 具體的實(shí)現(xiàn)方法
  • types 方法參數(shù)的編碼

我們?cè)俅芜\(yùn)行程序,日志如下:


動(dòng)態(tài)方法解析階段輸出日志

此時(shí),程序沒有crash,而是走了say這個(gè)方法,這是因?yàn)槲覀冊(cè)谶@個(gè)+ (BOOL)resolveInstanceMethod:(SEL)sel方法中給cat這個(gè)對(duì)象重新動(dòng)態(tài)添加了這個(gè)方法,所以,程序沒有崩潰,而是走了-(void)say這個(gè)方法。

二、備援接收階段實(shí)現(xiàn)

第二個(gè)階段我是在Dog類中進(jìn)行實(shí)現(xiàn)的,首先我在Dog.h文件中聲明了一個(gè)方法,它和Cat.h文件中的方法同名,同樣的,沒有在Dog.m文件中實(shí)現(xiàn)此方法,我們知道,如果對(duì)象在調(diào)用一個(gè)方法的時(shí)候沒有實(shí)現(xiàn)的話,那么程序會(huì)crash,可是我們可以通過(guò)重寫+ (BOOL)resolveInstanceMethod:(SEL)sel這個(gè)方法來(lái)防止crash,如果我們?cè)谶@個(gè)方法中返回No,那么,消息轉(zhuǎn)發(fā)就會(huì)進(jìn)入第二個(gè)階段,我們?cè)贒og.m中做如下操作:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
//    if (aSelector == @selector(sayHello)) {
//        return [[Cat alloc] init];
//    }
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sayHello"]) {
        return [[Cat alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

若方法是sayHello我們就創(chuàng)建一個(gè)接收者,然后改變此消息的接受者,只要在接受者那里有這個(gè)方法,程序就不會(huì)crash。我們?cè)贏nimalsViewController.m中實(shí)現(xiàn)調(diào)用此方法,代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    Cat *cat = [[Cat alloc] init];
    [cat sayHello];
    
    Dog *dog = [[Dog alloc] init];
    [dog sayHello];
}

運(yùn)行結(jié)果如下:

cat對(duì)象和dog對(duì)象調(diào)用sayHello方法打印的日志

我們可以看到,不管哪個(gè)對(duì)象調(diào)用sayHello方法,走的都是say方法。

三、完整消息轉(zhuǎn)發(fā)階段實(shí)現(xiàn)

當(dāng)前兩個(gè)階段都沒有成功處理這個(gè)消息時(shí),程序就會(huì)進(jìn)入第三個(gè)階段,此時(shí)我們使用Rabbit類來(lái)做具體的實(shí)現(xiàn)。首先,我們?cè)赗abbit.h中還是聲明-(void)sayHellog實(shí)例方法,依然不在在Rabbit.m中實(shí)現(xiàn),而我們可以在Rabbit.m文件中實(shí)現(xiàn)-(void)forwardInvocation:(NSInvocation *)anInvocation方法,具體代碼如下:

- (void)rabbitSayHello{
    NSLog(@"rabbitSayHello------%@",self);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sayHello"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
    // 改變對(duì)象
    [anInvocation invokeWithTarget:[[Cat alloc] init]];
    
    // 改變選擇子
//    anInvocation.selector = @selector(rabbitSayHello);
//    [anInvocation invokeWithTarget:self];
}

在這里要特殊說(shuō)明一下,當(dāng)進(jìn)入第三個(gè)階段之后我們需要先實(shí)現(xiàn)一個(gè)方法:- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,我們通過(guò)此方法對(duì)傳遞過(guò)來(lái)的消息進(jìn)行處理,此方法在- (void)forwardInvocation:(NSInvocation *)anInvocation方法之前調(diào)用,這是最后一個(gè)尋找IML的機(jī)會(huì),這個(gè)函數(shù)會(huì)讓重載方有機(jī)會(huì)拋出一個(gè)函數(shù)簽名,再由- (void)forwardInvocation:(NSInvocation *)anInvocation方法去執(zhí)行。我們?cè)贏nimalsViewController.m中調(diào)用:

- (void)viewDidLoad {
    [super viewDidLoad];

    Cat *cat = [[Cat alloc] init];
    [cat sayHello];
    
    Dog *dog = [[Dog alloc] init];
    [dog sayHello];
    
    Rabbit *rabbit = [[Rabbit alloc] init];
    [rabbit sayHello];
}

運(yùn)行程序,打印日志如下:

Cat對(duì)象、Dog對(duì)象和Rabbit對(duì)象同時(shí)調(diào)用sayHello方法打印的日志

從打印日志中我們可以看到,三個(gè)類最后都是調(diào)用的say方法。若三個(gè)階段都沒有成功處理sayHello方法,那么程序就會(huì)拋出crash。

消息轉(zhuǎn)發(fā)出現(xiàn)的場(chǎng)景

消息轉(zhuǎn)發(fā)出現(xiàn)在程序運(yùn)行時(shí)調(diào)用方法的時(shí)候,我們知道,當(dāng)調(diào)用的方法不存在的時(shí)候就會(huì)出現(xiàn)crash,而消息轉(zhuǎn)發(fā)機(jī)制的三個(gè)階段就是為了防止程序crash,只要我們?cè)谏鲜鋈齻€(gè)階段做了合理的處理,程序在運(yùn)行的時(shí)候就會(huì)動(dòng)態(tài)的處理這個(gè)消息,防止crash的出現(xiàn),若上述三個(gè)階段都沒有對(duì)消息進(jìn)行處理,就會(huì)進(jìn)入一個(gè)NSObject的一個(gè)方法
-(void)doesNotRecognizeSelector:(SEL)aSelector
在這個(gè)方法中捕獲異常,對(duì)異常進(jìn)行處理,獲取自己需要的信息。

若覺得此文章對(duì)您有用,記得給個(gè)喜歡呦??,[Runtime的demo]https://github.com/houcj/RuntimeWithMsgSend

附圖一張,查資料的時(shí)候考下來(lái)的

1444814548720164.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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