Objective-c 消息发送与消息转发

 

在Objective-c语言中,调用某个对象的方法,称为向某个对象发送消息。不同于C语言的函数调用,OC的方法调用是动态的,在运行时才决定调用具体的方法实现。

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        NSString * str = [[NSString alloc] init];
        NSLog(@"%@",str);
    }
    return 0;
}

将以上的OC代码通过clang -rewrite-objc转换为C/C++语言


int main(int argc, const char * argv[])
{
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSString * str = ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("init"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_32_bt767lwx1tvdwp4g5l1b3w2w0000gp_T_main_c3e70f_mi_0,str);
    }
    return 0;
}

可以看到OC代码中+alloc-init的方法调用的部分,转变为objc_msgSend()的函数调用

C语言的函数调用

#include <stdio.h>

void printHello(){
  printf("Hello World");
}

void printGoodBye(){
  printf("Good Bye");
}


void test(int a){
  if(a > 0) {
    printHello();
  } else {
    printGoodBye();
  }
}

C 语言在编译期就已经知道test()函数中会调用printHello()和printGoodBye()两个函数,会直接生成调用这些函数的指令,函数的入口地址会以硬编码的方法编译在指令中,这叫做静态绑定

#include <stdio.h>

void printHello(){
  printf("Hello World");
}

void printGoodBye(){
  printf("Good Bye");
}


void test1(int a){
  void(*func)();
  if(a > 0) {
    func = printHello;
  } else {
    func = printGoodBye;
  }
  func();
}

在编译期,test1()中会生成调用func函数的指令,但是func指向哪个具体的函数需要在运行期才能够确定,因此无法将函数入口地址硬编码到指令中。这就叫做动态绑定

OC的方法方法调用就属于动态绑定,在编译期硬编码到指令中的是objc_msgSend()的函数调用,而调用的具体方法实现在运行期确定。

消息发送 objc_msgSend()

objc_msgSend() 函数实现的伪代码大致如下:先确定对象的类,再确定方法的具体实现并调用

id objc_msgSend(id self, SEL _cmd, ...) {
  Class class = object_getClass(self);
  IMP imp = class_getMethodImplementation(class, _cmd);
  return imp ? imp(self, _cmd, ...) : 0;
}

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;
    if (!cls  ||  !sel) return nil;
    imp = lookUpImpOrNil(cls, sel, nil, YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }
    return imp;
}

class_getMethodImplementation 的大致实现:

  1. 首先会查询fast map快速映射表,如果找到则返回,未找到进入步骤2 (每一个类都有一块fast map的缓存,保存之前的匹配结果)

  2. 查询接受者所属类的方法列表,如果找到则返回,未找到进入步骤3

  3. 沿着继承体系继续向上查找,如果找到则返回,未找到进入消息转发的流程

消息转发

当一个OC对象收到一条消息,OC对象会在所属类的fast map方法列表以及在继承树中查询是否有对应的方法实现。如果能够找到,则直接调用方法实现;如果没有找到,则进入消息转发的流程。消息转发可以理解为在运行时发现消息没有对应的方法实现时,进行动态补救的过程。

动态方法解析(dynamic method resolution)

对象在收到无法解读的消息后,首先会进行动态方法解析,涉及以下两个方法:


@interface NSObject<NSObject>

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

@end

+resolveInstanceMethod:在无法找到实例方法实现时调用; +resolveClassMethod:在无法找到类方法实现时调用;

在这两个方法中可以在运行时为当前类添加sel的方法实现,前提是方法实现的代码在编译时已经存在。

//  例子

id dynamicGetter(id obj, SEL selector){
    return @"dynamicGetter";
}

void dynamicSetter(id obj, SEL selector,id value){
    NSLog(@"dynamicSetter %@",value);
}

@interface Test : NSObject

@property(nonatomic,strong) NSString * property1;

@end

@implementation Test

@dynamic property1;     // @dynamic 修饰属性,属性不生成对应的setter和getter方法

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString * selStr = NSStringFromSelector(sel);
    if([selStr isEqualsToString:@"setProperty1:"]){
        class_addMethod(self, sel, (IMP)dynamicSetter, "v@:");
        return YES;
    } else if([selStr isEqualsToString:@"property1"]) {
        class_addMethod(self, sel, (IMP)dynamicGetter, "@@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];     // 子类无法处理时,记得调用父类的resolveInstanceMethod
}

@end


在以上代码中,Test类声明了property1属性,但是用@dynamic修饰了property1属性,property1不会生成对应的setter和getter方法。

当试图调用property1的setter或者getter方法,在Test类的方法列表和继承树中都找不到对应的方法实现,就进入动态方法解析,调用+resolveInstanceMethod:。在这个方法中为property1的setter或者getter方法添加方法实现,方法实现是已经存在的函数。

+resolveInstanceMethod: 返回YES后,就重新走一遍消息发送的流程,此时property1的setter或者getter方法已经有了方法实现; 返回 NO 后,则继续消息转发的流程

-forwardingTargetForSelector:

动态方法解析无法处理未知消息,就会调用对象 -forwardingTargetForSelector:,试图将消息完整的转发给另一个对象,在方法中无法修改消息的内容(selector 和 参数)。

//  例子
@interface Test1 : NSObject

@property(nonatomic,strong) NSString * property1;

@end

@implementation Test1

@end



@interface Test : NSObject

@property(nonatomic,strong) NSString * property1;

@property(nonatomic,strong) Test1* test1;

@end

@implementation Test

@dynamic property1;     // @dynamic 修饰属性,属性不生成对应的setter和getter方法

- (id)forwardingTargetForSelector:(SEL)selector {
    if([selStr isEqualsToString:@"setProperty1:"] || 
       [selStr isEqualsToString:@"property1"]) {
         return _test1;   
    }

    return [super forwardingTargetForSelector:selector]; // 子类无法处理时,记得调用父类的forwardingTargetForSelector
               
}

@end

在以上代码中,当调用Testproperty1的setter或者getter方法,找不到方法实现,动态方法解析没有处理时,调用到-forwardingTargetForSelector:方法,在这里将消息完整转发给Test1类的实例。

注意: -forwardingTargetForSelector:不能返回self,否则会陷入无限循环

-forwardInvocation:

如果-forwardingTargetForSelector:不能够处理消息,就会调用到-forwardInvocation:

- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");

如果转发算法到了这一步,会启动完整的消息转发机制。首先会创建NSInvocation对象,NSInvocation对象会将receiverselector以及参数都封装起来,并作为参数传递到-forwardInvocation:方法。而在-forwardInvocation:方法中可以修改receiverselector以及参数,使得NSInvocation成为一次有效的调用。

实现-forwardInvocation:方法时,若发现某调用操作不应由本类处理,则需要调用父类的同名方法。这样继承体系中每个类都有机会处理此调用请求,直到NSObject。在NSObject-forwardInvocation:方法中,还会继而调用doseNotRecognizerSelector:抛出异常,表明消息最终未能处理。

消息转发的全流程

图1

在上图中, 接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能够在第一步就处理完,这样运行期系统就可以将此方法缓存起来。如果此类的实例收到同名的消息,那么根本无需启动消息转发流程。若想在第三步将消息转给备用的接受者,那么建议还是在第二步进行转发,第三步的代价会比第二步大得多。

总结

  • Objective-C的方法调用成为消息发送,消息由receiverselector以及参数组成。

  • Objective-C的消息发送是动态绑定的,在运行期确定方法实现。

  • Objective-C的消息发送实际上调用的是msg_send()函数,在类的fastmap方法列表以及继承体系中寻找方法实现。

  • 如果某次方法调用无法找到方法实现,就进入消息转发的流程。

  • 消息转发首先进行动态方法解析,调用+resolveInstanceMethod:或者+resolveClassMethod:,试图为消息添加对应的方法实现。

  • -forwardingTargetForSelector: 会完整的转发消息,不会修改消息的selector参数

  • 经过以上两步还不能处理消息,就调用-forwardInvocation:启动完整的消息转发机制,可以任意修改消息的receiverselector以及参数

参考文档

Objective-C 消息发送与转发机制原理

Effective Objective-C 2.0

这里贴上大神的消息发送和消息转发的流程图:

图1