Runtime 消息传递和消息转发

发表于:, 更新于:, By Rm1210
大纲
  1. 1. objec_msgSend 函数
    1. 1.1. objc_msgSend 过程一
    2. 1.2. objc_msgSend 过程二
    3. 1.3. objc_msgSend 过程三
  2. 2. 消息转发
    1. 2.1. 第一阶段:动态方法解析
    2. 2.2. 第二阶段:完整的消息转发机制
      1. 2.2.1. 备援接收者
      2. 2.2.2. 完整的消息转发
  3. 3. 总结
这篇为《Effective Objective-C 2.0》的读书笔记,涉及其中的第 1112

相信稍有经验的 Objective-C 程序员都会听说过:

Objective-C 的方法调用其实是消息传递

那这句话到底是什么意思呢?我们来看实际的例子:

1
[array insertObject:foo atIndex:5];
  1. array 是消息的接收者
  2. insertObject:atIndex: 是消息的 selector
  3. foo 和 5 是消息的参数
  4. selector 和 参数组合起来被称为 消息

所以上面的方法调用消息传递的方式可以表述为:

array 对象发送一条 selector 为 insertObject:atIndex:、参数为 foo 和 5 的消息。

objec_msgSend 函数

编译器看到这消息后会将其转换为标准的 C 语言函数调用,调用的函数原型为:

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

第一个参数为消息的接收者,第二个参数为消息的 selector(类型为 SEL),后面的参数为消息的参数(可以看到参数个数可变)。所以上面的例子会被编译器转换为:

1
2
3
4
objc_msgSend(array,
@selector(insertObject:atIndex:),
foo,
5);

objc_msgSend 需要在接收者所属的类中搜寻其方法列表

我们来看看这个搜索的过程:

objc_msgSend 过程一

1
2
3
4
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

首先,array 是一个实例对象,所以需要先从这个代表实例对象的结构体 objc_object 中根据 isa 指针找到其所属的类。

objc_msgSend 过程二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
...
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
...
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

然后,从代表其所属的类的结构体 objc_class 中的 methodLists 查找 @selector(insertObject:atIndex:)

objc_msgSend 过程三

1
2
3
4
5
6
7
8
9
10
11
struct objc_method_list {
...
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

在 methodLists 中怎么查找相应的 selector?可以看到,objc_method_list 其实是一个 objc_method 类型的可变数组,objc_method 中便一个 SEL 类型的 selector;如果找到了相应的 selector,便去执行 objc_method 中相应的实现:method_imp。

如果找不到相应的 selector 呢?那就沿着类的继承体系往上查找。

还是找不到呢?那就会走下节的消息转发过程。

消息转发

消息转发分为三大阶段:

第一阶段:动态方法解析

先征询消息接收者所属的类,看其是否能动态添加方法,以处理当前这个无法响应的 selector,这叫做动态方法解析(dynamic method resolution)。

这阶段涉及到的方法有:

1
2
3
4
// 针对实例方法
+ (BOOL)resolveInstanceMethod:(SEL)selector;
// 针对类方法
+ (BOOL)resolveClassMethod:(SEL)selector;

如果在这些方法中添加了响应的 selector 的实现并返回 YES,那 runtime 会重新启动一次消息发送过程。直接看书中的例子如下,演示了用 resolveInstanceMethod 来实现 @dynamic 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);
+ (BOOL)resolveInstanceMethod:(SEL)selector
{
NSString *selectorString = NSStringFromSelector(selector);
if (/* selector is from a @dynamic property */)
{
if ([selectorString hasPrefix:@"set"])
{
class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
}
else
{
class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
return [super resolveInstanceMethod:selector];
}

第二阶段:完整的消息转发机制

如果运行期系统(runtime system) 第一阶段执行结束,接收者就无法再以动态新增方法的手段来响应消息,进入第二阶段。

过了这个村就没这个店了。这个阶段已经不能动态添加方法了,真的只能转发了。这个阶段又分两小步:

备援接收者

看看有没有其他对象(备援接收者,replacement receiver)能处理此消息。如果有,运行期系统会把消息转发给那个对象,转发过程结束;如果没有,则启动完整的消息转发机制。

这阶段涉及到的方法有:

1
- (id)forwardingTargetForSelector:(SEL)aSelector;

如果在这个方法中返回的不是 nil 和 self,runtime 会向你返回的那个对象重新发送这条在此方法中被“截获”的消息。例子:

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(methodOfOtherClass:)){
return instanceOfThatClass;
}
return [super forwardingTargetForSelector:aSelector];
}
完整的消息转发

在此阶段。运行期系统会把与消息有关的全部细节都封装到 NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的消息。

这阶段涉及到的方法有:

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

首先,runtime 会发送 methodSignatureForSelector: 消息获取 selector 的相关信息。如果这个消息返回 nil,runtime 便会调用 NSObject 类的 doesNotRecognizeSelector: 来抛出异常,这便是我们经常看到的:

1
2
3
-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason:'-[NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87'

如果 methodSignatureForSelector: 返回一个 NSMethodSignature 实例,runtime 就会创建一个封装了所有消息细节(selector、target 和参数)的 NSInvocation 对象然后用这个对象调用我们的 forwardInvocation: 方法。

来看一个 ADLivelyCollectionView 中的例子:

1
2
3
4
5
6
7
8
9
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([_preLivelyDelegate respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:_preLivelyDelegate];
} else if ([_preLivelyDataSource respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:_preLivelyDataSource];
} else {
[super forwardInvocation:anInvocation];
}
}

上面的例子可以看到,如果某些消息我们不应该处理,应调用 super 的 forwardInvocation:,这样继承体系同的每个类都有机会捕获该消息,直至 NSObject 的 forwardInvocation:,NSObject 会在其实现中调用 doesNotRecognizeSelector: 以抛出我们熟悉的 unrecognized selector sent to instance 异常。

  • 如果只是单纯的转发消息,那放到 forwardingTargetForSelector: 中更为合理,因为这样不需要创建 NSInvocation。
  • 如果想转发消息前先修改消息内容,那只能在 forwardInvocation: 中来做,因为在 forwardingTargetForSelector: 中我们无法操作截获的消息。

总结

总结图片