iOS runtime--消息轉(zhuǎn)發(fā)

消息轉(zhuǎn)發(fā)概述

Objective-C是一門動態(tài)語言,怎么理解動態(tài)這一詞呢?簡單的說就是編譯器在編譯期可以只知道一個方法的名字,而不需要知道這個方法的實現(xiàn),只有在運行期間調(diào)用該方法的時候,才根據(jù)方法名去找到對應(yīng)方法的實現(xiàn),這個過程相當(dāng)于動態(tài)綁定一個方法的實現(xiàn),這就是“動態(tài)”。

與“動態(tài)”相對的是“靜態(tài)”,C語言就是一門靜態(tài)語言,在編譯期不僅知道運行時所要調(diào)用的函數(shù)的名字,而且直接生成了調(diào)用函數(shù)的指令,將函數(shù)地址硬編碼在這些指令中。這就是為什么OC的方法沒有實現(xiàn)只有聲明編譯不報錯,而C的方法卻報錯的原因。

正因為是動態(tài)綁定,所以在編譯期沒有報錯的程序,在運行時由于根據(jù)方法名找不到對應(yīng)的方法實現(xiàn),會導(dǎo)致程序的Crash,在Crash之前程序會依次調(diào)用幾個其他的方法,這就引出了消息轉(zhuǎn)發(fā)。

消息轉(zhuǎn)發(fā)過程

消息轉(zhuǎn)發(fā)是Objective-C語言的特點,當(dāng)一個對象在運行時接收到無法解讀的消息時,就會觸發(fā)“消息轉(zhuǎn)發(fā)”。

在編譯期向類發(fā)送無法解讀的消息是不會報錯的,因為在運行期可以繼續(xù)向類添加方法,所以在編譯期,編譯器無法知道到底有沒有某個方法的實現(xiàn)。
可能聽起來有點亂,又是向?qū)ο蟀l(fā)送無法解讀的消息,又是向類發(fā)送無法解讀的消息。前者可理解為只在運行期有的行為(因為有對象,那肯定是調(diào)用了生成對象方法的實現(xiàn)才可能存在,而只有運行期才去調(diào)用方法的實現(xiàn)),后者看下面這句代碼就知道了

[self performSelector:@selector(humenName)];

方法“humenName”我并沒有聲明也沒有實現(xiàn),但是這句代碼不會報錯(會報警告),編譯也可以通過,這就是所謂的編譯期向類發(fā)送無法解讀的消息。

消息轉(zhuǎn)發(fā)的過程是有一定的規(guī)則和步驟的。下面我們看看詳細(xì)的流程。

1.先看一個runtime庫的方法 class_addMethod

class_addMethod的用處是在程序運行時,給一個類添加方法實現(xiàn)的API,其完整API如下:

/** 
 * 根據(jù)指定的名字和方法實現(xiàn)給一個類添加方法.
 * 
 * @param cls 被添加方法的類.
 * @param name 指定要添加的方法名稱的選擇器。
 * @param imp 新的方法實現(xiàn),這個方法必須至少帶有兩個參數(shù):self和_cmd
 * @param types 上面那個新方法的參數(shù)的類型編碼. 
 * 
 * @return 當(dāng)方法添加成功返回YES , 否則返回NO 
 *  (例如,在類中已經(jīng)有一個該名字的方法實現(xiàn),會返回NO)
 *
 * @note class_addMethod 可能會覆蓋超類實現(xiàn), 如果超類也實現(xiàn)了該方法的話,
 * 但是不會替換在本類中已經(jīng)存在的方法實現(xiàn),
 * 如果改變本類存在的方法實現(xiàn),請使用method_setImplementation.
 */
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 

這個API在消息轉(zhuǎn)發(fā)中用到。消息轉(zhuǎn)發(fā)分為三個階段,即“動態(tài)方法解析”、“快速消息轉(zhuǎn)發(fā)”和“完整消息轉(zhuǎn)發(fā)機制”。

2. 動態(tài)方法解析

這里結(jié)合一個實例來說明,可能會更加容易懂些,新建一個項目,創(chuàng)建一個類HumenModel(繼承自NSObject),然后在ViewController中添加如下代碼

HumenModel *model = [HumenModel new];
[model performSelector:@selector(humenName)];

因為在HumenModel類中并沒有方法humenName的聲明和實現(xiàn),所以,對象model會接受到一個無法解析的消息,此時就會進入消息轉(zhuǎn)發(fā)的第一階段,征詢接收者所屬的類,看是否能動態(tài)添加方法,以處理這個未知的“選擇器”,此時就調(diào)用該類的類方法

+(BOOL)resolveInstanceMethod:(SEL)sel

當(dāng)然,如果是類接收到一個無法解析的消息,消息轉(zhuǎn)發(fā)第一階段調(diào)用的是類的另一個方法:

+(BOOL)resolveClassMethod:(SEL)sel

方法的參數(shù) sel 就是那個未知“選擇器”,返回值是BOOL類型,表示是否新增一個方法來處理未知“選擇器”。 現(xiàn)在我們就可以通過class_addMethod方法給類添加一個方法來處理未知“選擇器”,代碼如下:

+(BOOL)resolveInstanceMethod:(SEL)sel{
    
    // 獲取選擇器的方法名字
    NSString *selString = NSStringFromSelector(sel);
    if ([selString isEqualToString:@"humenName"]) {
        
        // 給接收者self 添加一個方法sayHello,選擇器sel指向方法的實現(xiàn),方法的類型編碼是v@:
        class_addMethod(self, sel, (IMP)sayHello, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
// 一個c函數(shù)
void sayHello(id self, SEL _cmd){
    NSLog(@"hello");
}

解釋下class_addMethod方法的第四個參數(shù)“v@:”
這是sayHello方法的OC類型編碼,‘v’表示返回值為void類型,‘@’表示第一個參數(shù)是對象,‘:’表示第二個參數(shù)是SEL類型的值,其中'@'和':'是固定的,因為每個方法都會有這兩個參數(shù)。對于其他情況可參照類型編碼這篇博客https://blog.csdn.net/ssirreplaceable/article/details/53376915

注意,所添加的方法必須是純C函數(shù)實現(xiàn)的,因為OC的方法名規(guī)則和C函數(shù)名規(guī)則差別是很大的。另外,在運行時,方法resolveInstanceMethod中的代碼會被動態(tài)插在類里面.

編譯運行,打印結(jié)果如下:

2019-02-20 17:31:53.411603+0800 MessageTrans[400:8535618] hello

跟預(yù)期的結(jié)果一樣,這樣就完成了動態(tài)方法解析,無法解析的消息在這一步得到了處理。如果在這階段沒有針對未知“選擇器”的做出處理,那么就會進入消息轉(zhuǎn)發(fā)的第二階段。

3.快速消息轉(zhuǎn)發(fā)機制

在這一階段,接收者將要甩鍋,看有沒有別的接收者可以處理這個無法解析的消息(記得將第一階段的代碼注釋掉)。這個一過程會在接收者所在類的下面這個方法中完成

-(id)forwardingTargetForSelector:(SEL)aSelector

參數(shù)aSelector是未知選擇器,返回值是id類型的值,所以這一階段只是針對對象來處理,不考慮類方法。

新建一個AnimalModel類,在其實現(xiàn)文件中實現(xiàn)方法humenName,如下:

-(void)humenName{
    NSLog(@"%s",__FILE__);
}

然后在HumenModel.m中實現(xiàn)forwardingTargetForSelector:方法,如下:

-(id)forwardingTargetForSelector:(SEL)aSelector{
    NSString *aSelectorString = NSStringFromSelector(aSelector);
    if ([aSelectorString isEqualToString:@"humenName"]) {
        return [AnimalModel new];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

這樣消息交由AnimalModel類處理,運行一下,打印結(jié)果如下:

2019-02-20 17:36:49.000246+0800 MessageTrans[2513:8550047] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m

結(jié)果正確,我們在第二階段成功的將消息轉(zhuǎn)發(fā)給其他接收者來處理。

對于AnimalModel類,不需要在其頭文件中聲明humenName方法,沒有影響。

如果沒有其他接收者,那就會進入“完整消息轉(zhuǎn)發(fā)機制”階段。

4.完整消息轉(zhuǎn)發(fā)機制

在這個階段要處理未知消息,代價就會大些,其實也是類似于快速消息轉(zhuǎn)發(fā)階段,目的都是指定一個接受消息的對象,只不過這里必須覆蓋兩個方法,即methodSignatureForSelector:和forwardInvocation:。

methodSignatureForSelector:的作用在于為另一個類實現(xiàn)的消息創(chuàng)建一個有效的方法簽名,必須實現(xiàn),并且返回不為空的methodSignature,否則會crash。
forwardInvocation:的作用是綁定消息接收者。

看代碼實現(xiàn):

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    if ([super methodSignatureForSelector:aSelector] == nil) {
        // 手動創(chuàng)建
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
    
    // 自動創(chuàng)建方法簽名
//    AnimalModel *animalModel = [AnimalModel new];
//    return [animalModel  methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    AnimalModel *model = [AnimalModel new];
    
    if ([model respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:model];
    }else{
        [self doesNotRecognizeSelector:anInvocation.selector];
    }
}

解釋下上面的代碼:方法簽名有兩種方式,一個是手動創(chuàng)建簽名,一個是自動創(chuàng)建簽名,看代碼可以明白,著重要講的是下面這句

NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];

參數(shù)值"v@:"為類型編碼,前面有提到,這里就不說規(guī)則了。原則上來說,該編碼的類型應(yīng)該與未知方法的參數(shù)對應(yīng),所以用根據(jù)未知方法自動創(chuàng)建方法簽名更好。但是如果非要用手動創(chuàng)建方法簽名的話,在寫方法signatureWithObjCTypes:的參數(shù)值時要注意兩點

  • 方法的返回類型必須有,不能省略,比如這里是返回空類型,所以對應(yīng)第一個編碼為v
  • 方法的默認(rèn)參數(shù)self和_cmd對應(yīng)的類型編碼不能寫錯,固定為"@:"

滿足上面兩點簽名就會有效,否則會導(dǎo)致crash,至于方法簽名中參數(shù)個數(shù)與未知方法不對應(yīng)是沒有問題的,比如說類型編碼為"v@:@@@",而未知方法為:humenName(沒有參數(shù)),是不會導(dǎo)致crash,只不過這里的參數(shù)寫多少個,會影響方法forwardInvocation:的參數(shù)值anInvocation的變化。

在第二個方法forwaidInvocation:必須判斷接收者是否能響應(yīng)未知消息,否則直接執(zhí)行[anInvocation invokeWithTarget:model]在接收者無法響應(yīng)位置消息時會導(dǎo)致崩潰。如果指定的接收者不能響應(yīng)未知選擇器,那么沒辦法了只能拋出異常,執(zhí)行doesNotRecognizeSelector:方法,程序崩潰。

這里在講下剛才提到的,方法簽名的參數(shù)個數(shù)的問題,看下面的代碼:

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    if ([super methodSignatureForSelector:aSelector] == nil) {
        // 手動創(chuàng)建
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@@:@@"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
    
    // 自動創(chuàng)建方法簽名
//    AnimalModel *animalModel = [AnimalModel new];
//    return [animalModel  methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    AnimalModel *model = [AnimalModel new];
    
    SEL sel = anInvocation.selector;
    NSMethodSignature *sign = anInvocation.methodSignature;
    NSLog(@"%lu--%@",(unsigned long)sign.numberOfArguments,NSStringFromSelector(sel));
    
    if ([model respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:model];
    }else{
        [self doesNotRecognizeSelector:anInvocation.selector];
    }
}

AnimalModel類中humenName方法實現(xiàn)如下:

-(void)humenName{
    NSLog(@"%s",__FILE__);
}

運行打印結(jié)果如下

2019-02-21 15:31:41.851617+0800 MessageTrans[14393:9247960] 4--humenName
2019-02-21 15:31:48.716371+0800 MessageTrans[14393:9247960] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m

根據(jù)打印結(jié)果可以看到,方法簽名中參數(shù)個數(shù),與anInvocation中的參數(shù)個數(shù)值是對應(yīng)的,并不與未知選擇器humenName的參數(shù)個數(shù)對應(yīng)(參數(shù)個數(shù)為0),而且這種不對應(yīng)并不會影響AnimalModel類中方法humenName的正常執(zhí)行。

拓展:如果對象調(diào)用一個未指定參數(shù)值的未知消息,但是在另一個類中有該方法的實現(xiàn),會怎樣呢
剛才我們調(diào)用的未知方法是沒有參數(shù)的,我們實現(xiàn)下有參數(shù)不指定值得代碼
viewController.m -> viewDidLoad

HumenModel *model = [HumenModel new];
[model performSelector:@selector(humenName:)];

AnimalModel.m

-(void)humenName:(NSInteger)number{
   NSLog(@"%s - %ld",__FILE__,(long)number);
}

HumenModel.m中的代碼和上面的相同,編譯運行,發(fā)現(xiàn)崩潰了,原因容易想到是這個消息沒有參數(shù)值,當(dāng)在AnimalModel中調(diào)用humenName:時,就會報野指針。這個問題怎么解決呢,見下面的代碼

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
   
   // 自動創(chuàng)建方法簽名
   AnimalModel *animalModel = [AnimalModel new];
   return [animalModel  methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
   AnimalModel *model = [AnimalModel new];
// 給anInvocation設(shè)置參數(shù)值
   NSInteger number = 10;
   [anInvocation setArgument:&number atIndex:2];// 為什么是2,因為0是self參數(shù),1是_cmd參數(shù),返回值類型不屬于參數(shù)。
   
   if ([model respondsToSelector:anInvocation.selector]) {
       [anInvocation invokeWithTarget:model];
   }else{
       [self doesNotRecognizeSelector:anInvocation.selector];
   }
}

這樣再運行看結(jié)果如下:

2019-02-21 16:13:56.024182+0800 MessageTrans[15139:9320442] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m - 10

運行正常

到這一步消息轉(zhuǎn)發(fā)的整個流程就講完了。

runtime底層代碼

消息轉(zhuǎn)發(fā)的實際應(yīng)用

1.多重代理
2.多重繼承
3.為 @dynamic修飾的屬性實現(xiàn)方法
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容