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拖到项目中即可,该文件会在运行时自动加载
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容