文章目录

  • 消息转发机制的前置条件
  • 消息转发机制
  • 1. 动态方法解析
  • 2. 备援接收者
  • 3. 完整的消息转发


消息转发机制的前置条件

首先要理解消息传递的概念

在OC中,方法的调用可以理解为对象接收消息,在这一过程中,采用动态绑定机制,即具体调用哪个方法要等到运行时才能确定并执行。

那么首先给对象发送消息

void returnValue = [someObject messageName:parameter];

语句发送消息后,编译器都会将其转化成对应的一条objc_msgSend C语言消息发送

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

在这个函数语句中,第一个参数表示填入消息的接收者,第二个参数就是消息的“选择子”,后面跟着可选的消息的参数,objc_msgSend 函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作,该方法需要接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转,如果最终还是找不到相符的方法。那就执行“消息转发”(message forwarding)操作。

消息转发机制

如果在消息传递过程中,接受者无法响应收到的消息,那么就会触发进入消息转发机制

消息转发机制主要分成三个步骤

1. 动态方法解析

(BOOL)resolveInstanceMethod:(SEL)selector;
//该方法参数就是那个未知的选择子,其返回值是Boolean类型,表示这个类是否能新增一个实例方法用以处理次选择子。

使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以来。
此方法常用来实现@dynamic属性,在运行期动态创建存取方法(@dynamic会告诉编译器,不要自动创建实现属性所用的实例变量,也不要为其创建存取方法)例如下:

Person.h
@interface Person : NSObject
id GetterName(id self, SEL cmd);
void SetterName(id self, SEL cmd, NSString *value);


+ (BOOL)resolveInstanceMethod:(SEL)sel;

- (void)eat;

- (void)sleep;

@property (nonatomic, strong) NSString *name;

@end
#import "Person.h"
#import <objc/runtime.h>
#import "Child.h"

@implementation Person
@dynamic name;



+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(name)) {
        // BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
        //@@: 此为签名符号
        class_addMethod(self, sel, (IMP)GetterName, "@@:");
        return YES;
    }
    if (sel == @selector(setName:)) {
        class_addMethod(self, sel, (IMP)SetterName, "v@:@");
        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

// (用于类方法)
//+ (BOOL)resolveClassMethod:(SEL)sel
//{
//    NSLog(@"resolveClassMethod called %@", NSStringFromSelector(sel));
//
//    return [super resolveClassMethod:sel];
//}

//get方法
id GetterName(id self, SEL cmd)
{
    NSLog(@"%@, %s", [self class], sel_getName(cmd));

    return @"Getter called";
}

//set方法
void SetterName(id self, SEL cmd, NSString *value)
{
    NSLog(@"%@, %s, %@", [self class], sel_getName(cmd), value);

    NSLog(@"SetterName called");
}

在此简单介绍一下class_addMethod方法

iOS OC消息转发 ios消息转发机制原理_消息转发

这个方法主要接受四个参数

Class cls 要添加方法的类
SEL name 被添加方法的名字
IMP imp 添加的方法的实现
const char *types 描述方法参数类型的字符数组。
// main.m
/* 现在在main.m中给Person发送setName:和name消息,由于Person中未实现这两个方法,就会经消息转发调用GetterName和SetterName方法
*/

Person *person = [[Person alloc] init];

[person setName:@"Jake"];

NSLog(@"%@", [person name]);

// 输出结果:

Person, setName:, Jake
SetterName called
Person, name
Getter called

若方法返回YES,则表示可以处理该消息。在这个过程中,可以动态的给消息增加方法。

若方法返回NO,则进行消息转发的第二步,查找是否有其他的接收者

2. 备援接收者

(id)forwordingTargetForSelector:(SEL)selector;

可以通过该函数返回一个可以处理该消息的对象。这种方法比较容易理解,请看例子:
我们先新建一个Child类,并在Child中实现一个eat方法
在Person类只定义不实现它(eat方法)

// Child.m

- (void)eat
{
    NSLog(@"Child method eat called");
}

在main中调用Person中的eat方法,由于Person类中并没有它的实现,使得在Person类中调用forwordingTargetForSelector,使它返回可以处理这个消息的Child类

// main.m

[person eat];


// Person.m
// 当调用Person中的eat方法时,由于Person中并未实现该方法,就会经下面的方法将消息转发给可以处理eat方法的对象

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSString *selStr = NSStringFromSelector(aSelector);

    if ([selStr isEqualToString:@"eat"]) {
        return [[Child alloc] init];        // 这里返回Child类对象,让Child去处理eat消息
    }

    return [super forwardingTargetForSelector:aSelector];
}

// 输出结果:

Child method eat called

这被叫做伪多继承。消息转发实现的伪多继承,对应的功能仍然分布在多个对象中,但是将多个对象的区别对消息发送者透明。

若第二步返回nil,则进入消息转发的第三步。

3. 完整的消息转发

(void)forwordInvocation:(NSInvocation *)invocation;

首先要利用methodSignatureForSelector用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的,生成方法签名可以先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择子,等等。

对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息 有关的全部细节都封装在anInvocation中(即这个签名),包括selector,目标(target)和参数。我们可以在forwardInvocation 方法中选择将消息转发给其它对象。

过程和逻辑都非常类似于第二种备援接收者的方法,下面直接上例子

// Person.m

//我们必须重写该方法 消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
     NSString *sel = NSStringFromSelector(aSelector);
    // 判断要转发的SEL
    if ([sel isEqualToString:@"sleep"]) {
        // 为转发的方法手动生成签名
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        //"v@:"解释一下:每一个方法会默认隐藏两个参数,self、_cmd,self代表方法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表返回值为void,@表示self,:表示_cmd。
    }

    return [super methodSignatureForSelector:aSelector]; 
}

//NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
//转发消息
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    //拿到消息(methodSignatureForSelector中生成的方法签名)
    SEL selector = [anInvocation selector];
    // 新建需要转发消息的对象 转发消息
    Child *child = [[Child alloc] init];
    if ([child respondsToSelector:selector]) {
        // 转发 唤醒这个方法
        [anInvocation invokeWithTarget:child];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
//从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。

// Child.h

#import <Foundation/Foundation.h>

@interface Child : NSObject

- (void)eat;

- (void)sleep;

@end

// Child.m

- (void)sleep
{
    NSLog(@"Child method sleep called");
}

// 输出结果:

Child method sleep called

这样也实现了消息转发。

iOS OC消息转发 ios消息转发机制原理_#import_02

上图即完整的消息转发机制

在消息转发机制中,虽有三层顺序关系(先执行第一种方法,前一套方案实现后一套就不会执行),但是在消息转发的过程中这三种办法都可以用来解决接受消息找不到对应的方法的情况。如果这三套方案都没有得以处理问题,那么程序就会crash。

参考文章:iOS消息转发与三次拯救