Runtime 消息传递和消息转发
这篇为《Effective Objective-C 2.0》的读书笔记,涉及其中的第 11、12 条
相信稍有经验的 Objective-C 程序员都会听说过:
Objective-C 的方法调用其实是
消息传递
那这句话到底是什么意思呢?我们来看实际的例子:
|
|
- array 是消息的
接收者
- insertObject:atIndex: 是消息的
selector
- foo 和 5 是消息的
参数
- selector 和 参数组合起来被称为 消息
所以上面的方法调用消息传递的方式可以表述为:
向 array
对象发送一条 selector 为 insertObject:atIndex:、参数为 foo 和 5
的消息。
objec_msgSend 函数
编译器看到这消息后会将其转换为标准的 C 语言函数调用,调用的函数原型为:
|
|
第一个参数为消息的接收者,第二个参数为消息的 selector(类型为 SEL),后面的参数为消息的参数(可以看到参数个数可变)。所以上面的例子会被编译器转换为:
|
|
objc_msgSend 需要在接收者所属的类中搜寻其方法列表
我们来看看这个搜索的过程:
objc_msgSend 过程一
|
|
首先,array 是一个实例对象,所以需要先从这个代表实例对象的结构体 objc_object
中根据 isa 指针找到其所属的类。
objc_msgSend 过程二
|
|
然后,从代表其所属的类的结构体 objc_class
中的 methodLists
查找 @selector(insertObject:atIndex:)
。
objc_msgSend 过程三
|
|
在 methodLists 中怎么查找相应的 selector?可以看到,objc_method_list 其实是一个 objc_method 类型的可变数组,objc_method 中便一个 SEL 类型的 selector;如果找到了相应的 selector,便去执行 objc_method 中相应的实现:method_imp。
如果找不到相应的 selector 呢?那就沿着类的继承体系往上查找。
还是找不到呢?那就会走下节的消息转发过程。
消息转发
消息转发分为三大阶段:
第一阶段:动态方法解析
先征询消息接收者所属的类,看其是否能动态添加方法,以处理当前这个无法响应的 selector,这叫做动态方法解析(dynamic method resolution)。
这阶段涉及到的方法有:
|
|
如果在这些方法中添加了响应的 selector 的实现并返回 YES,那 runtime 会重新启动一次消息发送过程。直接看书中的例子如下,演示了用 resolveInstanceMethod 来实现 @dynamic 属性:
|
|
第二阶段:完整的消息转发机制
如果运行期系统(runtime system) 第一阶段执行结束,接收者就无法再以动态新增方法的手段来响应消息,进入第二阶段。
过了这个村就没这个店了。这个阶段已经不能动态添加方法了,真的只能转发了。这个阶段又分两小步:
备援接收者
看看有没有其他对象(备援接收者,replacement receiver)能处理此消息。如果有,运行期系统会把消息转发给那个对象,转发过程结束;如果没有,则启动完整的消息转发机制。
这阶段涉及到的方法有:
|
|
如果在这个方法中返回的不是 nil 和 self,runtime 会向你返回的那个对象重新发送这条在此方法中被“截获”的消息。例子:
|
|
完整的消息转发
在此阶段。运行期系统会把与消息有关的全部细节都封装到 NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的消息。
这阶段涉及到的方法有:
|
|
首先,runtime 会发送 methodSignatureForSelector:
消息获取 selector 的相关信息。如果这个消息返回 nil,runtime 便会调用 NSObject 类的 doesNotRecognizeSelector:
来抛出异常,这便是我们经常看到的:
|
|
如果 methodSignatureForSelector:
返回一个 NSMethodSignature
实例,runtime 就会创建一个封装了所有消息细节(selector、target 和参数)的 NSInvocation 对象然后用这个对象调用我们的 forwardInvocation:
方法。
来看一个 ADLivelyCollectionView 中的例子:
|
|
上面的例子可以看到,如果某些消息我们不应该处理,应调用 super 的 forwardInvocation:
,这样继承体系同的每个类都有机会捕获该消息,直至 NSObject 的 forwardInvocation:
,NSObject 会在其实现中调用 doesNotRecognizeSelector:
以抛出我们熟悉的 unrecognized selector sent to instance
异常。
- 如果只是单纯的转发消息,那放到
forwardingTargetForSelector:
中更为合理,因为这样不需要创建 NSInvocation。 - 如果想转发消息前先修改消息内容,那只能在
forwardInvocation:
中来做,因为在forwardingTargetForSelector:
中我们无法操作截获的消息。