运行时消息传递与转发机制

前言

Objective-C是一门非常动态的语言,以至于确定调用哪个方法被推迟到了运行时,而非编译时。与之相反,C语言使用静态绑定,也就是说,在编译期就能决定程序运行时所应该调用的函数,所以在C语言中,如果某个函数没有实现,编译时是不能通过的。而Objective-C是相对动态的语言,运行时还可以向类中动态添加方法,所以编译时并不能确定方法到底有没有对应的实现,编译器在编译期间也就不能报错。

本文将简单介绍消息传递机制和消息转发机制。这需要我们首先了解类对象以及方法列表这个概念,不清楚的同学可以参考笔者之前的文章类的本质-类对象,希望对你有所帮助。本篇文章如有不实之处,还请指正!

(一)对象的消息传递机制 objc_msgSend()

在对象上调用方法在Objective-C中非常普遍。用Objective-C的术语来讲,这叫做“给某个对象发送某条消息”。消息有“名称”或“选择子(selector)”之说。消息可以接受参数,而且还可以有返回值。

在Objective-C中给对象发送消息是如下的格式:


本例中,someObject叫做方法调用者,也叫做接受者(receiver)。messageName:是方法名,也叫做选择子(selector)。选择子与参数合起来叫做”消息“(message)。在运行时,编译器会把上面这个格式的方法调用转化为一条标准的C语言函数调用,该函数就是鼎鼎有名的objc_msgSend(),该函数是消息objc里在运行时传递机制中的核心函数,其原型如下:


这是一个参数个数可变的函数。能接收两个或两个以上的参数,第一个参数代表接受者,第二个参数代表选择子。后续参数就是消息中的那些参数,其顺序不变。接受者就是调用方法的对象或者类(本质上类也是对象,叫做类对象)。选择子就是指的方法的名称,选择子和方法这两个词经常交替使用,其实是指的一个意思。运行时,上面Objc的方法调用会被翻译成一条C语言的函数调用,如下:


消息传递流程

objc_msgSend()函数会一句接受者(调用方法的对象)的类型和选择子(方法名)来调用适当的方法。

接收者会根据isa指针找到接收者自己所属的类,然后在所属类的”方法列表“(method list)中从上向下遍历。如果能找到与选择子名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。

如果找不到与选择子名称相符的方法,接收者会根据所属类的superClass指针,沿着类的继承体系继续向上查找(向父类查找),如果 能找到与名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。

如果在继承体系中还是找不到与选择子相符的方法,此时就会执行”消息转发(message forwarding)“操作。

(二)消息转发流程

上面讲到了对象的消息传递机制,如果在整个类的继承体系中还是找不到与选择子相符的方法,也就是对象或者类对象收到了无法解读的消息,那么就会进入到消息转发环节。

在编译期,向对象或者类对象发送了其无法解读的消息并不会报错,因为在运行期可以继续向类和元类(metaClass)中添加方法,所以编译器在编译期还无法确定类中到底会不会有某个方法的实现。当对象接收到无法解读的消息后,就会启动“消息转发(message forwarding)”机制,我们可以在消息转发过程中告诉对象应该如处理未知消息。

消息转发分为两个阶段。第一阶段叫做“动态方法解析(dynamic method resolution)”,或者叫“动态方法决议”。第二阶段涉及到“完整的消息转发机制(full forwarding mechanism)”,或者叫“完整的消息转发原理”。

(2.1)动态方法解析

动态方法解析的意思就是,征询消息接受者所属的类,看其是否能动态添加方法,以处理当前“这个未知的选择子(unknown selector)“。实例对象在接受到无法解读的消息后,首先会调用其所属类的下列类方法:


类对象在接受到无法解读的消息后,那么运行期系统就会调用另外的一个方法,如下:


如果运行期系统已经执行完了动态方法解析,那么消息接受者自己就无法再以动态新增方法的形式来响应包含该未知选择子的消息了,此时就进入了第二阶段——完整的消息转发。运行期系统会请求消息接受者以其他手段来处理与消息相关的方法调用。

(2.2)完整的消息转发

完整的消息转发又分为两个阶段,第一阶段称为备援接受者(replacement receiver),第二阶段才是启动完整的消息转发机制。

(2.2.1)备援接收者(replacement receiver)

当前接受者如果不能处理这条消息,运行期系统会请求当前接受者让其他接受者处理这条消息,与之对应的方法是:


方法参数代表未知的选择子,返回值为备援接受者,若当前接受者能找到备援接受者,就直接返回,这个未知的选择子将会交由备援接受者处理。如果找不到备援接受者,就返回nil,此时就会启用”完整的消息转发机制“。

(2.2.2)完整的消息转发

如果转发算法已经来到了这一步,那么代表之前的所有转发尝试都失败了,此时只能启用完整的消息转发机制。完整的消息转发机制是这样的:首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节封装于这个NSInvocation对象中。此对象中包含选择子(selector)、目标(target)及参数。在触发NSInvocation对象时,”消息派发系统(message-dispatch system)“将亲自触发,把消息派发给目标对象。此步骤中会调用下面这个方法来转发消息:


消息派发系统触发消息前,会以某种方式改变消息内容,包括 但不限于额外追加一个参数、改变选择子等。

实现此方法时,如果发现调用操作不应该由本类处理,则需要沿着继承体系,调用父类的同名方法,这样一来,继承体系中的每个类都有机会处理这个调用请求,直至rootClass,也就是NSObject类。如果最后调用了NSObject的类方法,那么该方法还会继而调用”doesNotRecognizeSelector:“以抛出异常,此异常表明选择子最终也未能得到处理。消息转发到此结束。

关于does NotRecognizeSelector:你可能感到陌生,但是对于类似于unrecognized selector send to instance xxx这样的错误,你可能并不陌生。这种错误通常是因为调用了某个对象或者某个类里不存在的方法,从而触发了消息转发机制,最终把这个未识别的消息发送给了NSObject的默认实现。

ps:方法缓存

通篇下来,发现调用一个方法并不像我们想的那么简单,更不像我们写的那么简单,一个方法的执行其实底层需要很多步骤。正因如此,objc_msgSend()会将调用过且匹配到的方法缓存在”快速映射表(fast map)“中,快速映射表就是方法的缓存表。每个类都有这样一个缓存。所以,即便子类实例从父类的方法列表中取过了某个对象方法,那么子类的方法缓存表中也会缓存父类的这个方法,下次调用这个方法,会优先去当前类(对象所属的类)的方法缓存表中查找这个方法,这样的好处是显而易见的,减少了漫长的方法查找过程,使得方法的调用更快。同样,如果父类实例对象调用了同样的方法,也会在父类的方法缓存表中缓存这个方法。

同理,如果用一个子类对象调用某个类方法,也会在子类的metaclass里缓存一份。而当用一个父类对象去调用那个类方法的时候,也会在父类的metaclass里缓存一份。

参考文章

《Effetive Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》第12条:理解消息转发机制

posted @ 2019-02-13 17:36  ios_福  阅读(70)  评论(0编辑  收藏  举报