发布于 

objc_msgSend和消息传递机制(大补)

在Objective-C上在对象上调用方法。用Objective-C大术语来说,这叫做「传递消息」(pass a message)。消息有「名称(name)」或「选择子(selector)」,可以接受参数。而且可能还有返回值。


动态绑定(dynamic binding) 待调用的函数地址无法硬编码在指令中,而是要在运行期读取出来。


在OC中,如果要向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法 。在底层,所有方法都是普通的C语音函数,然而在对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变。这些特性使得Objective-C成为一门真正的动态语言。

给对象发送消息会转换成

void objc_msgSend(id self, SEL cmd,…)

objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜索其“方法列表”(list of methods)如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到名称相符的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”

这么说来,想调用一个方法似乎需要很多步骤。所幸objc_msgSend会将匹配结果缓存在“快速映射表”里面。实际上,消息派发(message dispatch)并非应用程序的瓶颈所在。


刚才曾提到,objc_msgSend等函数一旦找到应该调用的方法实现之后,就会“跳转过去”。之所以能这样做,是因为OC对象的每个方法都可以视为简单的C函数,原型如下:

<return_type> class_selector(id self,SEL _cmd,…)

每个类里都有一张表格(参考下面第14条关于isa的描述),其中的指针都会指向这个函数,而选择子的名称则是查表时所用的“键”。objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和objc_msgSend很像。这不是巧合,而是利用“尾调用优化”(tail-call optimization)技术,令“跳至方法实现”这一操作变得更简单。

在实际编写OC时,无须担心这些问题,开发者应该了解其底层工作原理。代码究竟是如何执行的,而且能理解为何在调试的时候,栈信息中总是出现objc_msgSend


尾调用优化(tail-call optimization) 技术

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用推栈中推入新的“栈帧(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值来作他用时,才能执行“尾调用优化”。

这项优化对objc_msgSend非常关键,如果不这么做的话,那么每调用Objective-C方法之前,都需要为调用objc_msgSend函数准备「栈帧」,大家在“栈踪迹”(stack trace)中可以看到这种「栈帧」


理解消息转发机制

若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编译器向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接受到无法解读的消息后,就会启动“消息转发”(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。


动态方法解析

    对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
    +(BOOL) resolveInstanceMethod:(SEL)selector
    该方法的参数就是哪个未知的选择子,其返回值为Boolean类型,表示这个类是否新增一个实例方法处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,“resolveClassMethod”
    使用这种办法的前提是:相关方法的实现代码已经写好,只等待运行的时候动态插在类里面就可以了。此方案通常用来实现@dynamic属性,下面代码展示了如何使用“resolveInstanceMethod:”来实现@dynamic属性:


备援接收者

    当前接收者还有第二次机会处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来注册。

完整的消息转发

如果转发算法已经来到这一步,那么唯一能做的就是启动完整的消息转发机制了。首先创建NSInvacaton对象,把与尚未处理的那条消息有关的全部细节都封于其中,此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。

此步骤会调用下列方法转发消息:

-(void)forwardInvocation:(NSInvocation*)invocation

这个方法可以实现得很简单:只需要改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是该换选择子,等等。

实现此方法时,若发现某调用操作不应本类处理,则需要调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,次异常表示选择子最终未能得到处理。

消息转发全流程

下图描述了消息转发的各个步骤:

接收者在每一步均有机会处理消息。步骤越往后,消息处理的代价就越大。



本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站由 @shyiuanchen 创建,使用 Stellar 作为主题。