Objective-C 消息轉發機制

了解消息轉發機制,我們可以知道當我們給一個對象發送消息的時候,消息是怎么被調用的,可以知道消息的調用經歷了一個什么樣的過程,同時我們還可以應用運行時(runtime)來有效避免一些crash,具體如何避免,下面我將會用貼上代碼,代碼中有詳細的解釋。

下面我先說說運行時消息的轉發流程

消息在運行時的查詢流程

一圖勝千言,習慣性的先來一張圖以便對消息轉發有個整體的把握

運行時系統庫方法查詢

圖中提到對象會通過isa指針找到方法,它不是直接去查詢方法列表struct objc_method_list **methodLists,而是先查詢緩存struct objc_cache *cache,這樣優化了方法調用的速度,如果直接查詢的話,需要查詢本類,如果本類中沒有還需要通過super_class指針向上一級一級的查詢父類中有沒有需要的方法,這個過程要比先從緩存中查詢慢的多。

從下面是isa的結構體聲明代碼,可以看一下

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

當我們無論是從緩存中還是從函數列表中都查不到對應的方法時會執行resolveInstanceMethod或者resolveClassMethod方法,當這兩個方法都返回NO的時候,這時候系統就會走消息轉發的流程了。

系統提供了兩種消息轉發選項

快速轉發:

NSObject類的子類A可以通過重寫NSObject類的forwardingTargetForSelector:方法,將A的實例無法識別的消息轉發給目標對象B(也可以叫做 備援接收者 ),從而實現快速轉發。該技巧就像是將對象的實現代碼與轉發對象合并到一起。這類似于實現的多繼承行為。如果你有一個定了對象 能夠消化哪些消息的目標類,這個技巧可以取得很好的效果

標準(完整)轉發:

NSObject類的子類A可以通過重寫NSObject類的forwardInvocation:方法,實現標準轉發。標準轉發巧可以通過methodSignatureForSelector:方法獲取一個methodsignature對象最終被封為NSInvocation對象傳遞給forwardInvocation:方法(注意如果methodSignatureForSelector:方法返回一個nil,程序會crash)從該對象能獲取消息的全部內容(包含目標,方法名,和參數)。

還是喜歡上圖,下面是消息轉發全流程圖


消息轉發流程圖.png

代碼示例

寫了一大推字感覺很抽象,下面來點干貨
下面我要把Test實例的logName消息轉發給目標類Target,代碼如下
Test頭文件

#import <Foundation/Foundation.h>
@interface Test : NSObject
-(void)logName;
@end

Test實現文件

#import "Test.h"
#import "Target.h"
#import <objc/runtime.h>

@implementation Test {
    Target *mTarget;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        //創建目標對象
        mTarget = [Target new];
    }
    return self;
}

#if 0
//當一個對象無法識別消息后,會執行resolveInstanceMethod或者resolveClassMethod方法
//如果不想進行消息轉發,可以在此方法中動態添加消息來做處理
//如果不重寫此方法或者此方法返回NO,系統會執行forwardingTargetForSelector進行快速轉發

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(logName)) {
        //第四個參數詳解地址  https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        //v代表返回類型為void
        //@代表一個對象
        //:代表一個selector
        //因為OC中的每個方法都有默認的兩個參數sel 和 selector,所以一般都是v@:
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP,"v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//萬年備胎
void dynamicMethodIMP(id self, SEL _cmd)
{
    //對無法識別的消息做處理
    NSLog(@"該對象無法識別 %@ 方法------%s", NSStringFromSelector(_cmd),__func__);
}

#else 

/***************==========1、快速消息轉發,快速轉發只可以獲取到方法簽名==========*******************/

-(id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if ([mTarget respondsToSelector:aSelector]) {
        //目標對象有對應的處理方法,則就會快速消息轉發,不會再執行完整消息轉發了
        return mTarget;
    }
    //目標對象也沒有對應的方法,此時系統會執行forwardInvocation進行完整消息轉發
    return nil;
}

/***********=============2、標準(完整)消息轉發,完整消息轉發,可以獲取方法簽名,參數等詳細信息==========*********/

//根據參數aSelector 返回一個完整的包含該方法的簽名,找不到方法則返回nil
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
     if  (!signature)
        signature = [mTarget methodSignatureForSelector:aSelector];
     return signature;
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s-----完整消息轉發------",__func__);
    SEL invSEL = anInvocation.selector;
    if  ([mTarget respondsToSelector:invSEL]) {
        //利用forwardInvocation方法來重新指定消息處理對象
        [anInvocation invokeWithTarget:mTarget];
    } else { // 否者調用下面方法,下面的方法會拋出異常
        [self doesNotRecognizeSelector:invSEL];
    }
}
#endif

@end

目標文件的頭文件

#import <Foundation/Foundation.h>

@interface Target : NSObject
-(void)logName;
@end

目標文件的實現文件

#import "Target.h"

@implementation Target

-(void)logName{
    NSLog(@"我是備用方法---%s",__func__);
}

@end

總結

如果你定義了一個對象能夠轉發消息的目標類,快速轉發可以取得很好的效果。如果你沒有這樣目標類或想要執行其他處理過程(如記錄日志并‘吞下’消息),就應該使用完整轉發。

對于對象無法處理的消息,如果不做轉發處理的話,程序最終會調用NSObject的 doesNotRecognizeSelector:實例方法,這個方法會拋出unrecognized selector sent to instance異常,程序會crash掉。

擴展

  1. 簡單說一下NULL,nil,Nil,NSNull的用處
  • NULL:用于普通類型,例如NSInteger
  • nil:用于OC對象(除了類這個對象),給 nil 對象發送消息不會crash
  • Nil:用于Class類型對象的賦值(類是元類的實例,也是對象)
  • NSNull:常作為占位對象,一般會作為集合中的占位元素,

【注意】給NSNull對象發送消息會crash的,后臺給我們返回的<null>就是NSNull對象

  1. 推薦一個國外大大利用消息轉發避免crash的一個庫 NullSafe
    開發中你會發現有的后臺接口返回NSNull(控制臺打印出來是<null>這樣的形式)而引起的奔潰問題,例如你需要一個字符串他卻給你返回了一個“<null>”這樣一個NSNull對象。用法很簡單,直接把NullSafe.m拖到項目中即可,該文件會在運行時自動加載
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容