消息发送(Messaging)
8、以上便是runtime相关的一些数据结构,接下来我们回看一开始的疑问:
objc_msgSend()函数在执行的过程中是如何找到对应的类,找到对应的方法实现的呢?
这就是消息发送(messaging)的处理过程了:
(1)、对于上文的Class的数据结构的描述,官方文档只简略了把它们归纳成了两部分:一个指向其父类的指针和一个方法调用表(这个Class的所有方法的selector和实现代码所在地址的关联表);
(2)、当某个消息被发送到一个对象之后(即对象执行某个方法),runtime会根据这个对象的isa指针找到它所属的类,在类的方法调用表里查找对应的selector。如果没有找到的话,它会继续沿着类的super_class指针找到它的父类,在父类的方法调用表里查找对应的selector;
(3)、找到了对应的selector之后,就根据selector找到方法的实现代码的地址,执行这些实现的代码。如果没有找到,则会启用消息转发(message forwarding)机制,这个机制在后文会详谈;
(4)、所以一个方法的实现代码,并不是在编译的时候就确定好的,它是直到调用这个方法的时候,才通过消息发送机制,定位到方法的实现代码处执行,所以方法的调用和实现是动态绑定(dynamically bound)的;
(5)、当执行方法的实现代码的时候,objc_msgSend()函数不止会把实现代码需要的参数传给它,同时还会多传两个隐藏参数:self和_cmd。这两个参数其实就是objc_msgSend(receiver, selector)的receiver和selector,表面上objc_msgSend()函数只是把receiver和selector之后的那些参数传给了方法的实现代码(如果后面还有参数的话),实际上它偷偷地把receiver和selector也给传进去了,方法的实现代码里使用self和_cmd这两个形参就能调用到receiver和selector。
所以为什么当我们在编写一个方法的代码的时候,使用“self”就能直接调用到这个方法调用的对象,就是通过这个过程传递进来的;
(6)、为了提高消息发送的速度,每次在查找方法调用表前,会先查找一个类的cache(见前文7(7)),cache里存放了常用的方法的selector和实现代码地址的对应关系,如果在cache里能够找到对应的selector,那就可以直接跳到方法的实现代码处做执行,不需要再去跑剩下的消息发送流程。
判断方法是否“常用”依照了这样一个原则:如果一个方法被调用了一次,那么它就很有可能会被调用第二次,这个方法就会被加入cache。如果程序运行了足够久,让cache做了足够的热身(warn up),那么程序的运行会比一开始的时候更快,此时几乎所有需要调用的方法都能在cache里找到。
(7)、官方提供的消息发送的流程图如下:
动态方法解析(Dynamic Method Resolution)和消息转发(Message Forwarding)
9、那么还有一个疑问没有讨论,就是如果在消息发送的过程中发生了意外的话,它又会怎么处理呢?其实也就是8(3)中所提到的:如果消息发送没能找到对应的方法,那么runtime就会启用消息转发(message forwarding)机制来进行处理。
首先我们知道,正常情况下我们会在类的@implementation写好方法的实现代码,当执行这个方法的时候,runtime最终会绑定到这段实现代码并执行它,这是正常的流程。如果没有找到对应的实现代码,那么runtime会依次按照下面三个步骤来处理这个消息:
(1)、其实runtime并不会立刻就启动消息转发,首先runtime会做的是动态方法解析(Dynamic Method Resolution)。它调用当前类的类方法+resolveInstanceMethod:(处理实例方法)或+resolveClassMethod:(处理类方法),看看是否在方法中有动态添消息的方法实现,有则执行,无则继续下一步处理;
(2)、如果来到这一步,才是真正地开始消息转发了。runtime首先会进行快速转发(Fast Forwarding),它会调用当前类的- (id)forwardingTargetForSelector:方法,看看方法中是否有将此消息转发给其他类的处理,有则将消息转发给对应处理的类,无则继续下一步处理;
(3)、最后runtime会进行完整的消息转发(Normal Forwarding),它首先会调用- (NSMethodSignature *)methodSignatureForSelector:方法,如果方法能正常返回一个NSMethodSignature对象,那么它就会创建一个表示消息的NSInvocation对象,这个对象包含了消息相关的所有细节,然后调用- (void)forwardInvocation:方法进行完整的转发,如果- (void)forwardInvocation:方法中有对这个消息的相关转发处理,就将消息转发给对应的另一类进行处理处理,如果没有,则抛出unrecognized selector sent to instance或者unrecognized selector sent to class的异常信息。
这就是一个完整的消息转发处理流程。
10、我们可以通过@samlaudev的一个demo验证整个转发过程:
(1)、首先定义了一个Message类,并在类中定义了一个实例方法:
当调用了这个方法的时候:
会有如下输出:
这是一个正常的方法执行;
(2)、然后我们首先来验证第一步:动态方法解析。
将-(void)sendMessage:方法的实现代码注掉,同时添加以下方法:
这对应处理的第一步,此时-(void)sendMessage:方法已经没有正常的实现代码了,根据第一步,runtime会在+resolveInstanceMethod:方法中看看是否有动态添加-(void)sendMessage:方法实现,此时运行后输出:
说明runtime确实执行了动态方法解析;
(3)、然后我们来验证第二步,即是消息转发的第一步:快速转发给其他类处理。
此时需要新建一个其他类MessageForwarding,然后在MessageForwarding类中也定义一个-(void)message:方法:
然后回到Message类,把上一步的+resolveInstanceMethod:方法注掉,添加以下快速转发的方法:
意思即是将这个消息快速转发给MessageForwarding对象去处理,运行输出如下:
说明runtime执行了消息转发的第一步;
(4)、最后我们来验证处理的第三步,即是消息转发的第二步:将消息完整转发给其他类处理。
此时我们再新建一个类MessageNormalForwading,并在MessageNormalForwading类中也定义一个-(void)message:方法:
回到Message类,将第二步的-(id) forwardingTargetForSelector:方法注掉,然后添加以下两个方法:
将消息封装成一个NSIncocation对象,然后将它完整转发给MessageNormalForwading类去处理。执行后输出:
说明runtime完整地执行了消息转发的第二步。
由此我们验证了这三个步骤。
动态解析类方法和类型编码
11、在10(2)所处理的第一步中,如果需要动态解析的方法是类方法,应该怎么处理呢?
我们给Message类声明一个类方法+(void)classSendMessage:并且不做任何实现,然后需要在Message类中添加这样一个方法来处理:
执行以下代码后:
输出如下:
需要注意的地方是,在classSendMessage:方法内执行class_addMethod()函数时的第一个参数。
当我们添加实例方法的时候,class_addMethod()函数第一个参数传的是[self class],传当前的类;而添加类方法的时候,就需要传[self class]所属的类,当前类所属的类,即是元类(Meta Class)。
这正是我们在前文讨论过的,在类的method_list里添加方法,会成为它的实例可调用的方法,即是这个类的实例方法;在类所属的元类的method_list里添加方法,会成为元类的实例可调用的方法,元类的实例即是当前类,于是成为了这个类的类方法。
12、消息转发能让一个类通过把消息传递给其他类处理,来处理一些它本来不能处理的方法,看起来似乎能模拟“多重继承”的效果,通过把不同消息转发给其他类处理模拟了继承自其他类的效果。不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者,如respondsToSelector:和isKindOfClass:只能用于继承体系,而不能用于转发链。
13、还有一个地方可以注意一下:在动态方法解析和完整消息转发中的相关方法中,都出现了这么一个字符串:"v@*",这个字符串是类型编码,它将消息中的方法归纳成几个字符串来表示。
比如上文消息转发的例子中,消息里的方法是-(void)message:,于是"v@*"中的v表示方法返回值为void,*表示方法的参数是NSString类型的,@则表示隐藏参数self。
隐藏参数在类型编码中是可写可不写的,所以考虑到还有另外一个隐藏参数_cmd,这个类型编码写成"v@:*"也是可以的。当然直接写成"v*"也没问题。
参考文档:
官方文档
https://github.com/samlaudev/RuntimeDemo
http://www.jianshu.com/p/25a319aee33d
http://www.cocoachina.com/ios/20141105/10134.html
http://www.cocoachina.com/ios/20141106/10150.html