Objective-C运行时学习笔记-消息派发机制

之前的文章文章聊了一下Objective-C运行时机制的内容,这篇接着上篇文章最后的引子,继续聊一下 Objective-C 的运行时消息发送机制。

Objective-C 的消息发送机制全都依赖于运行时,这点是和新晋的 Swift 语言有很大的不同,Swift 的消息派发机制主要是依赖于静态绑定(static binding),即编译器明确的知道要执行哪个方法,直接生成在编译的时候就生成好跳转代码,而不需要在运行时决定到底执行哪个方法,这点是和 OC 消息派送最大的不同。

这也就是为什么在 Objective-C 中,我们不会使用「方法调用」来形容方法的被动执行,而是使用「消息传递」这个词。

方法调用: 通常与静态类型语言(如C++和Java)以及编译时绑定相关。

消息传递: 通常与动态类型语言(如Objective-C、Smalltalk和Python)以及运行时绑定相关。

# 消息发送 API

在OC上层语言调用中,我们消息传递的语法是 [receiver message],不过在编译过程中,会把这条语句编译为类似 objc_msgSend(object, @selector(message)) 这样的 API。

# objc_msgSend

objc_msgSend API 的 discussion 部分如下👇

当遇到方法调用时,编译器会生成对函数 objc_msgSendobjc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stret 之一的调用。发送到对象的超类(使用 super 关键字)的消息是使用 objc_msgSendSuper 发送的;其他消息使用 objc_msgSend 发送。将数据结构作为返回值的方法使用 objc_msgSendSuper_stretobjc_msgSend_stret 发送。

# Method & Selector & IMP

这三个属于的官方定义如下

Method (opens new window) 表示类定义中的方法的类型。

SEL (opens new window) 表示方法选择器(选择子)的不透明类型,本质上是个 C 的字符串,并且已经在运行时注册。但在使用选择器时,必须使用从 sel_registerName 或 Objective-C 编译器指令 @selector() 返回的值,而不能简单地将 C 字符串转换为 SEL

IMP (opens new window) 指向方法实现开始的指针。该数据类型是指向实现该方法的函数开头的指针,第一个参数是指向 self 的指针,第二个参数是 SEL,即方法选择器。

相关的定义如下:


/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
//objc.h
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// A pointer to the function of a method implementation.
typedef void (*IMP)(void /* id, SEL, ... */ );

在 runtime 的 objc_class 中的方法列表(method_list_t)里看到方法类型是 method_t。我理解 method_tobjc_method 类型应该是一个意思。

总的来说,MethodselectorIMP 的集合体。

SEL@selector

SEL 可以理解为类型,已编译的选择器被分配给特殊类型 SEL

@selector() 指令允许您引用已编译的选择器,而不是完整的方法名称。举个例子:

SEL setWidthHeight;  //声明 SEL 类型变量
setWidthHeight = @selector(setWidth:height:); //获取选择子

# 消息派送流程

尝试分析一下消息派送的流程,当消息发送到对象时,消息会现在对象所属类的体系中进行查找,如果没有找到的话就会触发消息转发机制。具体介绍如下👇

# 继承体系内查找

当向对象消息发送的时候,会通过对象的 isa 指针找到所属类结构,在类结构中查找方法列表(dispatch table)中的方法选择器。如果在那里找不到选择器, objc_msgSend 就会沿着指向超类的指针并尝试在其调度表中找到选择器。连续失败会导致 objc_msgSend 一直沿着类继承结构向上寻找,直到到达 NSObject 类。一旦找到选择器,该函数就会调用方法列表中的方法,并将其传递给接收对象的数据结构。

注意:这里的对象不仅仅包含类实例也包含类对象。

# 缓存机制

同时为了加快消息传递的过程,也引入了缓存机制,想想如果不去做方法缓存,一些较极端的情况下方法很多,继承体系又比较复杂,那消息查找的时间复杂度就是 O(N^2),缓存之后直接降到接近 O(N),所以缓存是非常必要的。

每个类都有一个单独的缓存,它可以包含父类以及当前类中的方法。在搜索调度表之前,消息传递例程首先检查接收对象类的缓存(理论上,使用过一次的方法可能会再次使用)。如果方法位于缓存中,则消息传递仅比函数调用稍慢(因为有个在缓存中查找消息的流程)。一旦程序运行了足够长的时间来“预热”其缓存,几乎它发送的所有消息都会找到缓存的方法。当程序运行时,缓存会动态增长以容纳新消息。

# 消息转发机制

如果没有 OC 的消息转发机制的话,我们给实例发送一个压根不存在的方法,运行后大概率会得到下面的异常提示:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[YourClass notExistMethod]: unrecognized selector sent to instance 0x600000010020'

有的时候则可能是我们不小心的疏忽导致了程序的崩溃,幸亏 Objective-C 提供了一个消息转发机制来让我们有更多的补救措施来避免类似情况的发生。理论上 Swift 这种静态语言是根本不会提供这种动态消息转发机制的。

消息转发机制有两个阶段

1️⃣ 动态方法解析(dynamic method resolution)。

当我们给某个类实例发送消息,而整个类继承体系都没有办法找到对应方法的时候,则会调用当前类的方法 + (BOOL)resolveInstanceMethod:(SEL)sel,此时我们可以在这个方法里面为当前类加入已经准备好的应对方法。

resolveInstanceMethod API 说明 Dynamically provides an implementation for a given selector for an instance method. Return YES if the method was found and added to the receiver, otherwise NO.

举个使用例子👇

@interface Student : NSObject
- (void)notExistMethod;
@end
@implementation Student
+ (BOOL)resolveInstanceMethod:(SEL)sel {
		//判断之后动态添加方法
    if (sel == @selector(notExistMethod)) {
          **class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");**
          return YES;
    }
    return [super resolveInstanceMethod:sel];
}
///提前预备好的C方法,因为OC方法至少带两个参数self和_cmd,所以这里也要带上数量
void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"dynamic method IMP");
}
@end
//main.m
Student *stu = [Student new];
[stu notExistMethod];

同理,如果消息是发给类对象,则会调用对应类方法 + (BOOL)resolveClassMethod:(SEL)sel

这里有个细节是,如果你没有在解析方法 resolveInstanceMethod 里添加选择子对应的实现的话,则这个解析方法会执行两遍,也很好理解,因为在向类添加了实例方法后,需要给消息派发系统一个机会去执行新添加的实例方法,所以消息派发机制会重新走一遍,如果还是没有找到对应选择子的实例方法,则会继续走一遍 resolveInstanceMethod 方法。

2️⃣ 备援接收者(fast message forwarding)

如果你没有对消息做上一步动态解析,运行时系统会给接受对象第二次机会来对消息进行处理。如果目标对象实现了 -forwardingTargetForSelector: ,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。

forwardingTargetForSelector API 说明 Returns the object to which unrecognized messages should first be directed.

如果一个对象实现或继承此方法,并返回非 nil 且非 self 结果,则返回的对象将用作新的接收者对象,并且完整执行新对象的消息派送流程。 显然,如果您从此方法返回 self,代码就会陷入无限循环。

举个使用例子👇

//🧑‍🏫 Techer
@interface Teacher: NSObject @end
@implementation Teacher
- (void)teach:(NSString *)subject { NSLog(@"teacher teach %@",subject); }
@end
//🧑‍🎓 Student
@interface Student : NSObject { Teacher *teacher; }
- (void)teach:(NSString *)subject;
@end
@implementation Student
- (instancetype)init {
    self = [super init];
    teacher = [Teacher new];
    return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(teach:)){
        return teacher;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end
//🤖具体执行
Student *stu = [Student new];
[stu teach:@"English"];
//🖥️打印结果
OCDemo[17517:555894] teacher teach English

还挺有意思的,官方的消息派发的文档 (opens new window)里似乎并没有提到这个消息快速转发机制。

3️⃣ 完整消息转发(normal message forwarding)。

如果你没有实现备援接收者方法,运行时系统会给接受对象最后一次机会来对消息进行处理,即运行时会给接收消息的对象那个发送 forwardInvocation: 消息,这个消息会将原始消息封装成一个 NSInvocation 对象,并将其作为方法参数传到该方法内。

- (void)forwardInvocation:(NSInvocation *)invocation; API 说明 Passes a given invocation to the real object the proxy represents.

我们就可以实现 forwardInvocation: 方法来提供对消息的响应,这个方法实现通常就是将消息转发给能处理这个消息的对象。

不过这时候如果已经错过了动态向类中添加实例方法(class_addMethod)的机会了,在 forwardInvocation: 里添加实例方法,并不会像 resolveInstanceMethod API 一样重新触发消息派发机制的从头执行,所以什么 API 做什么事,不要在转发消息的 API 里做添加实例方法的事情。

forwardInvocation: 的默认实现其实就是调用了 doesNotRecognizeSelector:,也就是我们在这小节刚开始提到的运行异常的提示消息。

举个使用例子👇

//🧑‍🏫 Techer
@interface Teacher: NSObject @end
@implementation Teacher
- (void)teach:(NSString *)subject { NSLog(@"teacher teach %@",subject); }
@end
//🧑‍🎓 Student
@interface Student : NSObject { Teacher *teacher; }
- (void)teach:(NSString *)subject;
@end
@implementation Student
- (instancetype)init {
    self = [super init];
    teacher = [Teacher new];
    return self;
}
/// 此方法必须实现,否则 forwardInvocation: 不会被调用,
/// Returns an NSMethodSignature object that contains a description of the method identified by a given selector.
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
		NSLog(@"methodSignatureForSelector %@",NSStringFromSelector(selector));
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [teacher methodSignatureForSelector:selector];
    }
    return signature;
}
/// 消息转发方法
- (void)forwardInvocation:(NSInvocation *)anInvocation {
		NSLog(@"forwardInvocation %@",anInvocation);
    if ([teacher respondsToSelector:anInvocation.selector]) {
        **[anInvocation invokeWithTarget:teacher];**
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end
//🤖具体执行
Student *stu = [Student new];
[stu teach:@"English"];
//🖥️打印结果
OCDemo[17008:532949] methodSignatureForSelector teach:
OCDemo[17008:532949] forwardInvocation <NSInvocation: 0x6000017002c0>
OCDemo[17008:532949] teacher teach English

整体的消息转发流程如下

Untitled

# 额外的话题

消息派发的流程基本上介绍完了,但是还有有一些和消息派发相关的话题,这里也聊一下

1️⃣ 消息转发与多重继承(Forwarding and Multiple Inheritance)

面向对象编程中的多重继承(multiple inheritance) 指的是一个类别可以同时从多于一个父类继承行为与特征的功能。与单一继承相对,单一继承指一个类别只可以继承自一个父类。 — 维基百科「多重继承」

消息转发从某种程度上模拟了多重继承。像下图里面,我们给 Warrior(战士) 类发送 negotiate(谈判) 消息,Warrior 可以将这个消息转发给 Diplomat(外交官) 实现。

Untitled

在外界看来,Warrior 类似乎是继承了 Diplomat 实现了 negotiate 方法,尽管它实际只是做了转发消息的过程。因此,Warrior 从继承层次结构的两个分支“继承”方法,它自己的分支和响应消息的对象的分支。假设 Warrior 是继承自 Army 类。则看起来的继承关系是

Untitled

消息转发提供了多重继承的大部分功能。 然而,两者之间有重要的区别:

① 从使用角度来说,多重继承倾向于将不同的功能组合在一个对象中,它倾向于大型、多面的对象;而转发则将不同的职责分配给不同的对象。 它将问题分解为更小的对象,但以对消息发送者透明的方式关联这些对象。

② 尽管消息转发模拟了继承,但是 NSObject 类并没有把这两个概念混在一起,像 respondsToSelector:isKindOfClass: 这类方法依然仅仅在继承体系内查找对应实现,比如我们像 Warrior 实例发送 respondsToSelector: 消息,就算 Warrior 实现了消息转发方法,但返回结果依然是 NO。如果想要返回不同的结果,则你需要重新实现 respondsToSelector:isKindOfClass: 这类方法。

2️⃣ 消息转发的底层实现机制。

大神 @draveness 分析了 objc_msgSend 的底层实现,在这里 (opens new window)。基本上底层实现和我们上面的分析的结果没有太大差异,对底层实现感兴趣的小伙伴,可以直接跳转去对应的文章看,这里不再赘述了。

# 总结

以上就是这篇文章的全部内容,简单概括 Objective-C 的的消息派发流程就是:

  • 尝试命中对象类的缓存;
  • 在对象类的继承体系方法列表中尝试找到该消息;
  • 通过实现动态解析方法来动态增加对应方法;
  • 实现 forwardingTargetForSelectoor: 进行快速转发;
  • 实现 forwardInvocation: 进行最后消息转发。

最后多说一句,就算是使用 Swift 语言,如果自定义类继承了 NSObject,也能获得动态消息派发的能力,参考上一篇我们对继承 NSObject 子类获得 OC 动态能力的说明。

参考地址:

  1. 运行学习笔记-基于运行时的继承体系
  2. Effective Objective-C 2.0 Chapter2 (opens new window)
  3. Friday Q&A 2009-03-27: Objective-C Message Forwarding (opens new window)
  4. Objective-C Runtime Programming Guide - Messaging (opens new window)
  5. @draveness-从源代码看 ObjC 中消息的发送 (opens new window)
  6. Glow 技术团队博客-Objective-C Runtime 消息发送 (opens new window)
  7. 理解 Swift 的方法派发 (opens new window)
  8. Swift Runtime分析:还像OC Runtime一样吗? (opens new window)
  9. A Deep Dive Into Method Dispatches in Swift (opens new window)
  10. Increasing Performance by Reducing Dynamic Dispatch (opens new window)

关注我的微信公众号,我在上面会分享我的日常所思所想。