Objective-C 深入理解中的消息机制和方法调用

  1. objc_msgSend 的背后
    1. 消息传递的过程
  2. 深入理解消息转发过程
    1. resolveInstanceMethod:
    2. forwardingTargetForSelector:
    3. forwardInvocation:
    4. forwardInvocation,forwardingTargetForSelector 的差异
      1. NSInvocation文档翻译
      2. forwardingTargetForSelector 相较于 forwardInvocation 的优点
  3. 来看看C++中的方法调度机制
    1. V-Table 和 dispatch table 的不同之处
    2. 其实 Objective-C 中也有 V-Table

objc_msgSend 的背后

在Objective-C中,消息在Runtime的时候才绑定到方法实现。编译器把

1
[receiver message]

转换成

1
objc_msgSend(receiver, selector)

这个messaging function 做了动态绑定所有的工作

消息传递的关键在于 编译器为每个class和object建立的 结构体.每个类的结构体包含着两个必备的元素:

实例中有一个isa指针,isa指向实例的class structure.

如图所示 通过isa指针,描述了类的继承关系
message1

消息传递的过程

  1. 当一个消息被发送给一个对象,messaging function跟随对象的isa指针找到他的class structure,在dispatch table中寻找method selector.
  2. 如果没有找到selector,objc_msgsend 跟随该类实例的isa找到父类,尝试在父类的dispatch table中寻找selector
  3. 重复步骤2,直到isa指向NSObject Class为止。

一但objc_msgsend定位到了selector,该函数,调用dispatch table中的方法,并将其传递给接收对象的数据结构。

这是Runtime中选择所需方法实现的实现方式.在面向对象编程的术语中,这些methods和messages 是动态绑定的。

为了加速消息传递的过程,runtime 系统缓存了使用过的 selectors 和 方法实现的地址。

每个class都有特定的cache.并且它可以包含 继承的selector和 定义在类中的方法。

在OC编程中,你向对象发送消息是因为该对象实现了相应的方法,并且你希望调用该方法。
在搜索dispatch tables之前,消息传递机制通常会首先检查接受方法的对象的cache。如果方法的selector在缓存中,调用method所需的时间仅仅比function call 稍微慢一点点
一但程序预热cache之后,大多数消息调用都能从cache中找到。为了存放更多的方法,Cache会动态的增长。

深入理解消息转发过程

以下是文档的中文翻译:

resolveInstanceMethod:

resolveInstanceMethod: 和 resolveClassMethod: 方法允许你为一个给定的 selector 动态的提供方法的实现。
OC 方法在底层的C函数的实现中需要至少两个参数:self 和 _cmd。使用 class_addMethod 函数,你能够添加一个函数到一个类来作为方法使用。

forwardingTargetForSelector:

如果一个对象实现了这个方法,并且返回了一个非空(以及非 self)的结果,返回的对象会用来作为一个新的接收对象,随后消息会被重新派发给这个新对象。(很明显,如果你在这个方法中返回了self,那这段代码将会坠入无限循环。)
如果你这段方法在一个非 root 的类中实现,并且如果这个类根据给定的selector什么都不作返回,那么你应该返回一个 执行父类的实现后返回的结果。
这个方法为对象在开销大的多的 forwardInvocation: 方法接管之前提供了一次转发未知消息的机会。这对你只是想简单的重新定位消息到另一个对象是非常有用的,并且相对普通转发更快一个数量级。如果转发的目的是捕捉到NSInvocation,或者操作参数,亦或者是在转发过程中返回一个值,那这个方法就没有用了。

forwardInvocation:

当对象接受到一条自己不能响应的消息时,运行时会给接收者一次机会来把消息委托给另一个接收者。他委托的消息是通过NSInvocation对象来表示的,然后将这个对象作为 forwardInvocation: 的参数。接收者收到 forwardInvocation: 这条消息后可以选择转发这个NSInvacation对象给其他接收对象。(如果这个接收对象也不能响应这条消息,他也会给一次转发这条消息的机会。)
因此 forwardInvocation: 允许在两个对象之间通过某个消息来建立关系。转发给其他对象的这种行为,从某种意义上来说,他“继承”了他所转发给的对象的一些特征。注意为了响应这个你无法识别的方法,你除了 forwardInvocation: 方法外,还必须重写 methodSignatureForSelector: 方法。在转发消息的机制中会从 methodSignatureForSelector: 方法来创建NSInvocation对象。所以你必须为给定的 selector 提供一个合适的 method signature ,可以通过预先设置一个或者向另一个对象请求一个。

forwardInvocation,forwardingTargetForSelector 的差异

相较于 forwardingTargetForSelector 只能拿到 selector 来说, forwardInvocation 借助于invocation 可以获得参数和返回值等信息。

NSInvocation文档翻译

NSInvocation:NSInvocation对象用于在对象之间和应用程序之间存储和转发消息,主要由NSTimer对象和分布式对象系统来完成。 NSInvocation对象包含Objective-C消息的所有元素:目标,选择器,参数和返回值。每个元素都可以直接设置,并在调度NSInvocation对象时自动设置返回值。

一个NSInvocation对象可以重复地分派给不同的目标;它的参数可以针对不同结果的调度时进行修改;甚至它的选择器可以变为另一个具有相同的方法签名(参数和返回类型)。这种灵活性使得NSInvocation对重复发送具有许多参数和变体的消息有用;而不是为每条消息重新输入一个稍微不同的表达式,每次在将其分派到新目标之前,都需要根据需要修改NSInvocation对象。

NSInvocation不支持具有可变数量的参数或联合参数的方法的调用。您应该使用invocationWithMethodSignature:类方法来创建NSInvocation对象;你不应该使用alloc和init来创建这些对象。

此类不保留默认包含的调用的参数。如果这些对象可能在创建NSInvocation实例和使用它的时间之间消失,则应该自己保留对象或调用retainArguments方法以使调用对象保留它们本身。

总结一下:可以拿到参数和返回值,可以改selecter(方法签名相同),可以把消息分发给多个对象。相较于 forwardingTargetForSelector 只能拿到selector 灵活了很多。

forwardingTargetForSelector 相较于 forwardInvocation 的优点

This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.

简而言之,只做简单的消息转发时, 它比forwardInvocation 对性能的消耗要少。

我们需要注意

  1. forwardInvocation,forwardingTargetForSelector 都会给指定的对象 再走一次 objc_msgsend流程。
  2. forwardInvocation 相较于 forwardingTargetForSelector 消耗更大
  3. forwardInvocation 可以拿到参数和返回值,可以改selecter(方法签名相同),可以把消息分发给多个对象。相较于 forwardingTargetForSelector 只能拿到selector 灵活了很多。

来看看C++中的方法调度机制

直接调度
直接调度是最快的一种。它不仅有最少的汇编指令,而且编译器也可以做各种智能优化,比如内联代码,许多其它本文不会涉及的东西。直接调度也常常被称为静态调度。

表调度
表调度是编译型语言中最常见的动态行为的实现方式。表调度在类声明中为每一个方法用一个函数指针的数组。大多数语言称之为“虚表”。每一个子类都有它自己的一张父类表的拷贝,表中每个被覆盖的方法都是不同于父类的函数指针。当子类添加新的方法的时候,这些方法就被追加到这个数组的后面。然后在运行时就会访问这个表来决定执行哪个方法。

V-Table 和 dispatch table 的不同之处

这里的虚表 与 Objective-C 中的dispatch table 完全是两个概念.
虚表包含了当前类和父类的函数指针。
dispatch table 仅仅包含了当前类的selector和MethodIMP的地址。

其实 Objective-C 中也有 V-Table

其实OC中也有V-Table 它用来存放 最常调用的方法来加速程序的性能。这些方法直接通过V-table 调用。(执行msg_send()会花费更多的时间)
每一个对象都有一个vtable point 指向 一些方法的IMP.
objc-runtime-new.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/***********************************************************************
* vtable dispatch
*
* Every class gets a vtable pointer. The vtable is an array of IMPs.
* The selectors represented in the vtable are the same for all classes
* (i.e. no class has a bigger or smaller vtable).
* Each vtable index has an associated trampoline which dispatches to
* the IMP at that index for the receiver class's vtable (after
* checking for NULL). Dispatch fixup uses these trampolines instead
* of objc_msgSend.
* Fragility: The vtable size and list of selectors is chosen at launch
* time. No compiler-generated code depends on any particular vtable
* configuration, or even the use of vtable dispatch at all.
* Memory size: If a class's vtable is identical to its superclass's
* (i.e. the class overrides none of the vtable selectors), then
* the class points directly to its superclass's vtable. This means
* selectors to be included in the vtable should be chosen so they are
* (1) frequently called, but (2) not too frequently overridden. In
* particular, -dealloc is a bad choice.
* Forwarding: If a class doesn't implement some vtable selector, that
* selector's IMP is set to objc_msgSend in that class's vtable.
* +initialize: Each class keeps the default vtable (which always
* redirects to objc_msgSend) until its +initialize is completed.
* Otherwise, the first message to a class could be a vtable dispatch,
* and the vtable trampoline doesn't include +initialize checking.
* Changes: Categories, addMethod, and setImplementation all force vtable
* reconstruction for the class and all of its subclasses, if the
* vtable selectors are affected.
**********************************************************************/
static const char * const defaultVtable[] = {
"allocWithZone:",
"alloc",
"class",
"self",
"isKindOfClass:",
"respondsToSelector:",
"isFlipped",
"length",
"objectForKey:",
"count",
"objectAtIndex:",
"isEqualToString:",
"isEqual:",
"retain",
"release",
"autorelease",
};
static const char * const defaultVtableGC[] = {
"allocWithZone:",
"alloc",
"class",
"self",
"isKindOfClass:",
"respondsToSelector:",
"isFlipped",
"length",
"objectForKey:",
"count",
"objectAtIndex:",
"isEqualToString:",
"isEqual:",
"hash",
"addObject:",
"countByEnumeratingWithState:objects:count:",
};

script>