一、正常的消息传递
在C等语言中,调用一个方法就是执行内存中的一段代码,这在编译的时候就决定好了,所以没有动态的特性,而在Objective-C中,方法的调用会被编译成消息的发送,调用某个对象的方法,其实是在运行时给对象发送了一条消息, 例如,下面的代码是等价的:
[array insertObject:obj atIndex:0];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 0);
那么,runtime又是如何处理objc_msgSend发送的消息呢?从发送消息到最终的方法调用,这之间经历了一个怎样的过程?
objc_msgSend的源码是使用汇编写的,本人汇编渣,无法从源码角度去分析,不过从详尽的注释和相关的文档,我们可以一窥其面目,objc_msgSend会经过以下步骤:
1、通过对象的isa指针找到对象所属的class
2、在class的方法缓存(objc_cache)中查找方法,如果没有找到,则继续3、4步骤
3、在class的method_list中查找方法
4、如果class中没有找到方法,则继续往它的super class中查找, 直到查找到根类
5、一旦找到方法,就去执行对应的方法实现(IMP),并把方法添加到方法缓存中
在这个方法查找过程中,runtime引入了缓存机制,这是为了提高方法查找的效率,因为,如果调用的方法在根类中,那么每次方法调用都要沿着继承链去每个类的方法列表中查找一遍,效率无疑是低下的。这个方法缓存的实现,其实就是一个哈希表的存储,以selector name的哈希值为索引, 存储方法的实现(IMP),这样的查找效率较高,看到这里,可能有人会有疑问,既然每个class维护着一个方法缓存的哈希表,为什么还要维护一个方法列表method list呢?每次直接去哈希表里查找方法不是更快吗?
这其实是因为哈希表是一个无序表,而方法列表是一个有序列表,查找方法会顺着method list依次查找,这样就赋予了category一个特性:可以覆盖原本类的实现,而如果是使用哈希表,则无法保证顺序。关于category的原理和具体实现,将在后续的文章中探讨。
二、异常的消息传递
2.1、普通的函数调用
Person * person = [[Person alloc]init];
[person A];
如果 函数A 未声明方法
结果:编译失败
2.2、利用消息传递来调用函数
objc_msgSend(person, @selector(resolveThisMethodDynamically));
结果:编译成功
原因:你可以随时对一个对象传递任何消息,而不需要在编译的时候声明这些方法。所以Objective-C可以在runtime的时候传递消息。
在上面的查找方法的过程中,如果最终没有查找到目标方法,会导致crash,但是在crash之前,runtime会给我们2次机会去挽救程序:
Dynamic Method Resolution
Message Forwarding
总结
Objective-C中给一个对象发送消息会经过以下几个步骤:
在对象的类中查找selector,如果找到了,执行对应函数的IMP,查找过程会使用缓存,并且沿着类的继承关系链查找
如果没有找到,Runtime 会发送+resolveInstanceMethod:或者+resolveClassMethod:尝试去 resolve 这个消息
如果 resolve 方法返回 NO,Runtime 就发送-forwardingTargetForSelector:允许你把这个消息转发给另一个对象
如果没有新的目标对象返回, Runtime 就会发送-methodSignatureForSelector:和-forwardInvocation:消息。你可以发送-invokeWithTarget:消息来手动转发消息或者发送-doesNotRecognizeSelector:抛出异常。
demo地址:https://github.com/lionsom/LXRunTimeAll 项目007
此demo只简单的验证的了“Dynamic Method Resolution”
“Message Forwarding”未成功!!
参考文档:http://zziking.github.io/ios/2016/02/09/Objective-C_Runtime_2_Messaging_and_forwarding.html