Runtime - 消息发送原理
Runtime - 消息发送原理.
Objective-C运行时的核心就在于消息分派器objc_msgSend,消息分派器把选择器映射为函数指针,并调用被引用的函数。 要想理解objc_msgSend的背后原理,先来理解下NSInvocation这个类。
NSInvocation是命令模式的一种传统实现,它把一个目标、一个选择器、一个方法签名和所有的参数都塞进一个对象里,这个对象可以先存储起来,以备将来调用。当NSInvocation被调用时,它会发送信息,Objective-C运行时会找到正确的方法实现来执行。我们通过一个例子来理解下NSInvocation的作用,比如[NSObject alloc],此时会发送一个alloc消息,这条消息都包含什么内容呢?它怎么找到alloc的实现方法呢?这些都是通过NSInvocation来完成的,它包含了消息要传递的内容,也告诉了该怎么找到对应的方法实现。
解释一下什么是方法实现?一个方法实现(IMP)是一个指向具有如下签名的C函数的函数指针,注意是指针。
1
|
id function (id self, SEL _cmd, ...) |
NSInvocation包含了一个目标和选择器,目标是一个可接受的对象,选择器则是被发送的消息。比如[NSObject alloc],目标就是NSObject,选择器就是alloc。一个选择器大致是一个方法的名称,之所以说是大致是因为选择器不必精确映射到方法。比如[NSString length]和[NSData length]会映射到不同方法的实现,但他们拥有相同的选择器。
NSInvocation还包含一个方法签名(NSMethodSignature),它封装了一个方法的返回类型和参数类型,记住它不包括方法名称,只有返回类型和参数类型。你可以手动创建一个方法签名,如下:
1
|
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes: "@@:*" ]; |
但是应该尽可能少使用signatureWithObjCTypes:方法,获得方法签名常用的方法是为它请求一个类或实例,比如可以使用methodSignatureForSelector:方法从实例中请求实例方法签名,或者从类中请求类方法签名。也可以使用instanceMethodSignatureForSelector:方法从一个类中获取实例方法签名。两个方法有点绕口,我们通过一个例子来看下区别:
1
2
3
4
5
6
7
8
9
|
SEL initSEL = @selector(init); SEL allocSEL = @selector(alloc); // 从NSString类中获取实例方法(init)的方法签名 NSMethodSignature *initSig = [NSString instanceMethodSignatureForSelector:initSEL]; // 从test实例中获取实例方法(init)的方法签名 initSig = [@ "test" methodSignatureForSelector:initSEL]; // 从NSString类中获取类方法签名 NSMethodSignature *allocSig = [NSString methodSignatureForSelector:allocSEL]; |
最后,NSInvocation还包含了所有的参数。至此,对于[NSString length]和[NSData length]就可以通过NSInvocation对象包含的信息,找到它们分别对应的方法实现。我们来看一个具体的例子,如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
NSMutableSet *set = [NSMutableSet set]; NSString *stuff = @ "stuff" ; SEL selector = @selector(addObject:); NSMethodSignature *sig = [set methodSignatureForSelector:selector]; 44 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; [invocation setTarget:set]; [invocation setSelector:selector]; [invocation setArgument:&stuff atIndex:2]; [invocation invoke]; 44 NSLog(@ "set is : %@" , set); |
注意,第一个参数被置于索引2处,索引0是目标(self),索引1是选择器(_cmd),NSInvocation会自动设置它们。另外,必须把参数指针传递给参数,而不能传递参数本身。
接下来重点要介绍下消息传递是如何工作的?
在Objective-C中调用方法最终会翻译成调用方法实现的函数指针,并传递给这个方法实现一个对象指针、一个选择器和一组函数参数。每个Objective-C消息表达式都会转化为对objc_msgSend的调用,看下objc_msgSend的工作方式:
1. 检查接受对象是否为nil,如果是nil,调用nil处理程序。
2. 检查缓存中是不是已经有方法实现了,有的话,直接调用。
3. 比较请求的选择器和类中定义的选择器,如果找到了,调用方法实现。
4.比较请求的选择器和父类中定义的选择器,然后是父类的父类,以此类推,如果找到了选择器,调用方法实现。
5. 调用resolveInstanceMethod:(或resolveClassMethod)。如果它返回YES,那么重新开始。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。
6. 调用forwardingTargetForSelector:,如果返回非nil,那就把消息发送到返回的对象上,这里不要返回self,否则会形成死循环的。
7.调用methodSignatureForSelector:,如果返回非nil,创建一个NSInvocation并传给forwardInvocation:。
8. 调用doesNotRecognizeSelector:,默认的实现是抛出异常。
先看下第5步,首先可以想到的就是用resolveInstanceMethod:和resolveClassMethod:在运行时提供实现,这通常是@dynamic合成属性的处理方式。简单来说,就是需要自己实现属性的getter和setter方法,通过resolveInstanceMethod:方法来把setter方法和getter方法和属性绑定在一起。
如果第5步返回NO的话,系统接着会首先尝试一次快速转发,也就是调用forwardingTargetForSelector:,看其能否返回一个对象,如果有对象返回,就转发给返回的对象。快速转发的原理其实就是先从缓存里找下是否存在对应的选择器。
如果快速转发返回nil的话,接下来就进行普通的转发,调用forwardInvocation进行普通的转发。
objc_msgSend还有几个相关的函数:objc_msgSend_fpret、objc_msgSendSuper、objc_msgSend_stret、objc_msgSendSuper_stret。SendSuper格式的函数很明显是把消息发送给父类,而带stret的在返回结构体时处理大部分情况。在Intel处理器上返回浮点数时,带fpret的函数处理大部分情况。
方法混写(Method Swizzling)与ISA混写(Isa Swizzling)
在Objective-C中,混写(Swizzling)是指透明地把一个东西换成另一个,我们可以利用Objective-C中的运行时来实现混写。我们先看下方法混写,Objective-C提供了以下API来动态替换类方法或实例方法的实现:
-
class_replaceMethod替换类方法的定义。
-
method_exchangeImplementations交换两个方法的实现。
-
method_setImplementation设置一个方法的实现。
我们来看下三者的区别:
-
class_replaceMethod,当需要替换的方法有可能不存在时,可以考虑使用该方法。
-
method_exchangeImplementations,当需要交换两个方法的实现时使用。
-
method_setImplementation是最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。
系统中提供的KVO使用到了isa混写,具体是怎么实现的呢?当你观察一个对象时,一个新的类会被自动创建,这个新类继承自该对象的原本的类,并且重写了被观察属性setter方法。重写setter方法会负责在调用原setter方法之前和之后,通知所有观察对象:值的更改。最后通过isa混写,把这个对象的isa指针指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
注意一点:把isa指针指向新创建的子类,被观察的对象就变成了新创建子类的对象实例,这是由于isa指针永远指向其对应的类。用一张图来说明下:
键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:。在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变之后,didChangeValueForKey:会被调用,继而obserValueForKey:ofObject:change:context:也会被调用。可以手动实现这些调用,但很少有人这么做。一般我们希望能控制回调的调用时机时才会这么做。
使用KVO的一个明显的优势就是零开销观察的优势,如果给定的实例没有观察者,那么KVO不会有任何消耗,因为根本没有KVO代码。而即使没有观察者,对于委托方法和NSNotification还得工作。