NSOiOOC对象模型及运行时(2/3)-消息 2014-01-11
Part1主要讲了Objective-C的对象模型以及怎样动态创建一个类,稍微说了点运行时的消息传递,这一部分将要重点说一下运行时以及其中最重要的一个概念-消息,以及消息是怎么传递、转发的。
运行时
运行时是一套开源的C和汇编代码库,是Objective-C的运行系统,它能为编译好的Objective-C代码提供强大的动态特性;运行时由两套版本:lagecy version
和 modern version
,在64bit的 OS X 及iOS平台都是使用的modern version
,当然后者的功能更强大一些。
那么怎样才能和运行时系统进行交互呢?Objective-C提供了三种方法。
- 不知不觉的交互
运行时其实无处不在,只是有时候我们可能并没有感觉到它的存在而已;当我们向一个对象发送任意一条消息
,背后就是运行时控制去完成的。
- 间接的交互
Fondation框架NSObject对象定义了一些自省方法,这些方法都是和运行时去打交道。比如:和类的继承关系有关的一些方法isKindOfClass:
,isMemberOfClass:
,判断能不能响应某一个方法,是不是符合某一个协议的:respondsToSelector:
、conformsToProtocol:
还有对象之间进行相比较的isEqual:
,这些都是由运行时去完成的。
- 直接的交互
通过导入头文件<objc/runtime.h>
,我们就可以直接使用运行时来进行编程了,其方法大致一共分下面几类,和类相关的方法(只说前缀) class_
,和对象相关的方法:objc_
,和方法相关的:method_ sel_ imp_
,和协议相关的:protocol_
和属性相关的 property_
。
消息
在第一种交互方式中,我们发送一条消息,运行时在中间是怎么工作的呢?其过程主要分为以下几步:
- 编译器的转换
当我们向一个对象发送一条消息,
1 |
|
第一条指令编译器会帮我们变成这样
1 | id objc_msgSend(id self, SEL _cmd, ...) |
1 | /// 不透明结构体, selector |
其中,self是消息接受者,即receiver,_cmd是selector,…是可变参数,这就是一个标准的C函数,它的指针我们用IMP来表示,其实,我们向要调用的方法最终也是一个C函数,;我们向一个对象发送一条消息目的是要执行某一个方法,到了运行时那里,就变成了根据消息接收者和selector去寻找函数指针IMP,并且调用执行它。而且运行时会隐含给我们所调用的方法发送两个隐含参数:self和_cmd
,我们可以在方法中使用它。
当向一般对象发送消息时,调用objc_msgSend;当向super发送消息时,调用的是objc_msgSendSuper; 如果返回值是一个结构体,则会调用objc_msgSend_stret或objc_msgSendSuper_stret。
这里需要注意的一点时,super只是编译器的一个提示符,让运行时直接去调用父类的方法,其消息接收者是self;面试题:[super class]返回什么?详细细节可参考这里
- objc_msgSend的执行过程 - 消息处理
为了加快速度,苹果对这个方法做了很多优化,这个方法是用汇编实现的。伪代码表示:
1 | id objc_msgSend(id self, SEL op, ...) { |
objc_msgSend的动作比较清晰:首先在Class中的缓存查找imp(没缓存则初始化缓存),如果没找到,则向父类的Class查找。如果一直查找到根类仍旧没有找到,则用_objc_msgForward函数指针代替imp。最后,执行这个imp。
_objc_msgForward是用于消息转发的。这个函数的实现并没有在objc-runtime的开源代码里面,而是在Foundation框架里面实现的。
这就是消息的处理过程,官方的图如下所示:
消息转发
由消息的处理过程我们知道,当运行时系统通过接收者和selector去寻找函数指针,直到NSObject类都没有找着的时候,就会跳转到消息转发,如果消息转发之后依然没有处理,也没被吞掉,则程序会执行doesNotRecognizeSelector:
,然后crash。在程序crash之前,程序员一共有三次机会进行手动转发,分别是:
- 使用动态方法解析
1 | + (BOOL)resolveInstanceMethod:(SEL)selector |
消息转发的第一步,当转发的是实例方法
时,运行时给接收者发送上面的消息,询问开发者是否进行方法解析,此处可以通过运行时提供的
1 | class_addMethod(self,selector,(IMP)autoDictionarySetter,"v@:@"); |
来动态为类指定的selector添加一个实现,然后并执行返回,随后会把这对selector-IMP缓存起来。如果转发的是类方法时,运行时会调用下面的方法,执行过程一样。如果没有添加实现的话,执行下一步.
1 | + (BOOL)resolveClassMethod:(SEL)sel); |
如果在声明一个属性之后,我们在impenmention文件中使用了@dynamic
关键字,意思是告诉编译器一不要synthesize属性,二不要报错,属性的getter/setter方法我会在消息转发时给你找到。那么这个时候,我们就可以在动态方法解析中为用@dynamic定义的属性添加setter或setter方法。
- 使用备援接收者(快速转发)
1 | - (id)forwardingTargetForSelector:(SEL)aSelector); |
如果第一步没有添加实现的话(即使retune YES也白搭,运行时目前的实现只是用这个返回值来判断是否打印Log),运行时会给开发者第二次机会去用一个备援接收者去接收这个selector,然后把这个接收者返回;如果能找到的话就执行,如果没有找到,或者返回的是nil,运行时会接着执行第三步。
- 完整的消息转发
1 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; |
这个过程分为两步:
运行时给接收者发送消息
methodSignatureForSelector:
来获取方法签名,方法签名知识一个对方法类型
的封装,包含了参数个数,参数类型,返回值类型,是否属于oneway
等信息,运行时会根据这个方法签名及aSelector
生成一个NSInvocation
对象,然后继续朝下传递。如果返回nil,则运行时会给接收者发送doesNotRecognizeSelector:
,程序会crash.运行时根据
方法签名及SEL
封装成NSInvoation
,然后给接收者发送forwardInvocation:
,这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与备援接收者方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换selector
,等等。如果实现这个方法而不对NSinvocation
作处理的话,这条消息就意味者被吞掉。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject
。如果最后调用了NSObject
类的方法,那么该方法还会继而调用doesNotRecognizeSelector:
以抛出异常,此异常表明选择子最终未能得到处理。
消息转发与多重继承
在第二种情况-使用备援接收者时,就能很明显的看到多重继承的影子,如果要实现的更完整一些,可以参考下面官方给出的范例:
- 第一步 :重写自省方法
因为在程序中需要进行如下的判断:
1 | if ( [aWarrior respondsToSelector:@selector(negotiate)] ) |
所以我们要重写respondsToSelector:
isKindOfClass:
instancesRespondToSelector:
conformsToProtocol:
等方法
1 | - (BOOL)respondsToSelector:(SEL)aSelector { |
- 第二步 : 生成方法签名
1 | - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { |
- 第三步 : 转发
1 | - (void)forwardInvocation:(NSInvocation*)invocation { |
总结
运行时对消息的处理可归结为:自动寻找+三次手动转发
通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
对象可以把其无法解读的某些选择子转交给其他对象来处理。
经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
消息转发的原则是:越早处理越好,因为越往后成本越高。
参考资料
2.Objective-C Runtime Programming Guide
3.Understanding the Objective-C Runtime
5.objc_msgSend() Tour Part 1: The Road Map