IOS 消息传递与消息转发

1、方法method和selector(选择子)有什么关系

在 Objective-C 中,selector,Method 和 implementation(IMP) 都是 Runtime 的组成部分。在实际开发中它们常常是可以相互转换来处理消息的发送的。选择子代表方法在 Runtime 期间的标识符。为 SEL 类型,虽然 SEL 是 objc_selector 结构体指针,但实际上它只是一个 C 字符串。在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-C 的 Runtime 运行系统。
得出结论:选择子其实是方法的名称,不同类中方法名相同参数不同的俩个方法,他们的选择子是相同的。

Method的结构体

/// Method
struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
};
  • 方法名 method_name 类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型 method_types 是个 char 指针,其实存储着方法的参数类型和返回值类型,即是 Type Encoding 编码。(即类型编码)
  • method_imp 指向方法的实现,本质上是一个函数的指针,就是前面讲到的 Implementation。

消息传递

在OC中,给对象发送消息的语法是:

id returnValue = [someObject messageName:parameter];

这里,someObject叫做接收者(receiver),messageName:叫做选择子(selector),选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:

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

第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的·,所以这个函数就是参数个数可变的函数。

因此,上述以OC形式展现出来的函数就会转化成如下函数:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

可以看出,在调用方法时,编译器将它转成了objc_msgSend消息发送了,在Runtime的执行过程如下

  • 1、Runtime先通过对象someobject找到isa指针,判断isa指针是否为nil,为nil直接return。
  • 2、若不为空则通过isa指针找到当前实例的类对象,在类对象下查找缓存是否有messageName方法。
  • 3、若在类对象缓存中找到messageName方法,则直接调用IMP方法(本质上是函数的指针)。
  • 4、若在类对象缓存中没找到messageName方法,则查找当前类对象的方法列表methodlist,若找到方法则将其添加到类对象的缓存中。
  • 5、若在类对象方法列表中没找到messageName方法,则继续到当前类的父类中以相同的方式查找(即类的缓存->类的方法列表)。
  • 6、若在父类中找到messageName方法,则将IMP添加到类对象缓存中。
  • 7、若在父类中没找到messageName方法,则继续查询父类的父类,直到追溯到最上层NSObject。
  • 8、若还是没有找到,则启用动态方法解析、备用接收者、消息转发三部曲,给程序最后一个机会
  • 9、若还是没找到,则Runtime会抛出异常doesNotRecognizeSelector。

综上,方法的查询流程基本就是查询类对象中的缓存和方法列表->父类中的缓存和方法列表->父类的父类中的缓存和方法列表->...->NSObject中的缓存和方法列表->动态方法解析->备用接收者->消息转发。

消息动态解析

Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。前者在 对象方法未找到时 调用,后者在 类方法未找到时 调用。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。

主要用的的方法如下:

// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;

/** 
 * class_addMethod    向具有给定名称和实现的类中添加新方法
 * @param cls         被添加方法的类
 * @param name        selector 方法名
 * @param imp         实现方法的函数指针
 * @param types imp   指向函数的返回值与参数类型
 * @return            如果添加方法成功返回 YES,否则返回 NO
 */
BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                const char * _Nullable types);

测试代码

main.m 文件中
  int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    Person *xiaoming = [[Person alloc]init];
    [xiaoming performSelector:@selector(way)];
    return 0;
    
}

person.m 文件中
  // 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(way)) {
        class_addMethod([self class], sel,class_getMethodImplementation([self class], @selector(method)), "123");//使用class_addMethod动态添加方法method
    }
    return YES;
}
- (void)method{
    NSLog(@"进入了消息动态解析");
}
运行了上述的测试代码后,我们会发现即便我们并没有实现way方法,而且使用了performSelector去强行调用way方法,但是我们的程序并没有崩溃,是因为在查找了类方法和所有的父类后还是没有找到way方法,程序进入了消息动态解析,然后我们使用了class_addMethod去动态添加方法method,最后程序从调用

performSelector和直接调用方法的区别

performSelector: withObject:是在iOS中的一种方法调用方式。他可以向一个对象传递任何消息,而不需要在编译的时候声明这些方法。所以这也是runtime的一种应用方式。

所以performSelector和直接调用方法的区别就在与runtime。直接调用编译是会自动校验。如果方法不存在,那么直接调用 在编译时候就能够发现,编译器会直接报错。
但是使用performSelector的话一定是在运行时候才能发现,如果此方法不存在就会崩溃。所以一般使用performSelector的时候,一般都会使用- (BOOL)respondsToSelector:(SEL)aSelector;来在运行时判断对象是否响应此方法。

消息接受者重定向(备用接受者)

如果上一步中 +resolveInstanceMethod: 或者 +resolveClassMethod: 没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。

如果当前对象实现了 -forwardingTargetForSelector: 或者 +forwardingTargetForSelector: 方法,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。

其中用到的方法。

// 重定向类方法的消息接收者,返回一个类或实例对象
+ (id)forwardingTargetForSelector:(SEL)aSelector;
// 重定向方法的消息接收者,返回一个类或实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector;

注意:

  1. 类方法和对象方法消息转发第二步调用的方法不一样,前者是+forwardingTargetForSelector: 方法,后者是 -forwardingTargetForSelector: 方法。
  2. 这里+resolveInstanceMethod: 或者 +resolveClassMethod:无论是返回 YES,还是返回 NO,只要其中没有添加其他函数实现,运行时都会进行下一步。

测试代码

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(way)) {
        Friends *friends = [[Friends alloc]init];
        return friends;//返回friends对象,让friends对象接受这个消息
    }
    return [super forwardingTargetForSelector:aSelector];
}

可以看到,虽然当前 person 没有实现 fun 方法,+resolveInstanceMethod: 也没有添加其他函数实现。但是我们通过 forwardingTargetForSelector 把当前 person 的方法转发给了 friends 对象去执行了。打印结果也证明我们成功实现了转发。

我们通过 forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程。

消息重定向

如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector: 或者 +methodSignatureForSelector: 方法获取函数的参数和返回值类型。

  • 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
  • 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。

所以我们可以在 forwardInvocation: 方法中对消息进行转发。

注意:类方法和对象方法消息转发第三步调用的方法同样不一样。
类方法调用的是:

  1. + methodSignatureForSelector:
  2. + forwardInvocation:
  3. + doesNotRecognizeSelector:

对象方法调用的是:

  1. - methodSignatureForSelector:
  2. - forwardInvocation:
  3. - doesNotRecognizeSelector:

用到的方法:

// 获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(way)) {
        return [NSMethodSignature methodSignatureForSelector:@selector(way)];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    Friends *f = [[Friends alloc] init];
    if([f respondsToSelector:sel]) {   // 判断 Person 对象方法是否可以响应 sel
        [anInvocation invokeWithTarget:f];  // 若可以响应,则将消息转发给其他对象处理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然无法响应,则报错:找不到响应方法
    }
}

既然 -forwardingTargetForSelector: 和 -forwardInvocation: 都可以将消息转发给其他对象处理,那么两者的区别在哪?

区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。

以上就是 Runtime 消息转发的整个流程。

消息发送以及转发机制总结

调用 [receiver selector]; 后,进行的流程:

  1. 编译阶段:

    [receiver selector]; 方法被编译器转换为:

    1. objc_msgSend(receiver,selector) (不带参数)
    2. objc_msgSend(recevier,selector,org1,org2,…)(带参数)
  2. 运行时阶段:消息接受者

    recevier寻找对应的 selector

    1. 通过 recevier 的 isa 指针 找到 recevier 的 class(类);
    2. 在 Class(类) 的 cache(方法缓存) 的散列表中寻找对应的 IMP(方法实现);
    3. 如果在 cache(方法缓存) 中没有找到对应的 IMP(方法实现) 的话,就继续在 Class(类) 的 method list(方法列表) 中找对应的 selector,如果找到,填充到 cache(方法缓存) 中,并返回 selector;
    4. 如果在 class(类) 中没有找到这个 selector,就继续在它的 superclass(父类)中寻找;
    5. 一旦找到对应的 selector,直接执行 recevier 对应 selector 方法实现的 IMP(方法实现)。
    6. 若找不到对应的 selector,Runtime 系统进入消息转发机制。
  3. 运行时消息转发阶段:

    1. 动态解析:通过重写 +resolveInstanceMethod: 或者 +resolveClassMethod:方法,利用 class_addMethod方法添加其他函数实现;
    2. 消息接受者重定向:如果上一步添加其他函数实现,可在当前对象中利用 forwardingTargetForSelector: 方法将消息的接受者转发给其他对象;
    3. 消息重定向:如果上一步没有返回值为 nil,则利用 methodSignatureForSelector:方法获取函数的参数和返回值类型。
      1. 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
      2. 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容