格而知之7:我所理解的Runtime(2)
消息发送(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
www.jianshu.com/p/25a319aee33d
http://www.cocoachina.com/ios/20141105/10134.html
http://www.cocoachina.com/ios/20141106/10150.html
格而知之7:我所理解的Runtime(2)的更多相关文章
- 格而知之3:Core Data的基本使用
最近准备做一个随手笔记类的app给自己用,考虑到从未使用过Core Data,就决定用Core Data来做数据存储.在网上参考了一些Core Data的资料后,用一天的时间写了这个demo,主要测试 ...
- 理解 Objective-C Runtime
初学 Objective-C(以下简称ObjC) 的人很容易忽略一个 ObjC 特性 —— ObjC Runtime.这是因为这门语言很容易上手,几个小时就能学会怎么使用,所以程序员们往往会把时间都花 ...
- 理解Objective-C Runtime(四)Method Swizzling
Objective-C对象收到消息之后,究竟会调用何种方法需要在运行期间才能解析出来.那你也许会问:与给定的选择子名称相应的方法是不是也可以在runtime改变呢?没错,就是这样.若能善用此特性,则可 ...
- 格而知之16:我所理解的Block(2)
11.那么Block到底是怎么实现的呢?试一试通过将Block 的代码转换成普通C语言代码来查看它的实现过程. 要将OC代码转换成C语言代码,可以使用clang编译的一个命令: 通过这个命令能把指定文 ...
- 格而知之6:我所理解的Runtime(1)
基本简介 1.根据官方文档,OC有一个特性:它会尽可能把一些决定从编译时和链接时推迟到运行时才处理,所以这门语言需要的就不只是一个编译器,它还需要一个runtime系统来处理那些已经被编译过的代码. ...
- 格而知之16:我所理解的Block(3)
23.在前文中的例子中,Block结构体里的isa指针还没有详细讲解,这个指针都被置向了_NSConcreteStackBlock,它标识了Block的类型. 其实除了_NSConcreteStack ...
- 格而知之15:我所理解的Block(1)
1.Block 本质上是一个struct结构体,在这个结构体中,最重要的成员是一个函数(当然除函数外还有其他重要的成员). 2.在开始解析Block之前,首先来回顾一下Block的格式.Block相关 ...
- 格而知之8:我所理解的Runtime(3)
关联对象 14.使用Category对类进行拓展的时候,只能添加方法,而不适合添加属性(可以添加属性,也可以正常使用get方法和set方法,只是不会自动生成以下划线开头命名的成员变量). 可以通过关联 ...
- 格而知之5:我所理解的Run Loop
1.什么是Run Loop? (1).Run Loop是线程的一项基础配备,它的主要作用是来让某一条线程在有任务的时候工作.没有任务的时候休眠. (2).线程和 Run Loop 之间的关系是一一对应 ...
随机推荐
- mysql 编码设置
(windows下) 打开C:\Program Files\MySQL\MySQL Server 5.0\my.ini (ubuntu下) 打开 /etc/mysql/my.cnf 在[client] ...
- javascript 继承机制设计思想
作者: 阮一峰 原文链接:http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_java ...
- mac下识别国产android手机
mac下识别国产android手机困扰了我很久,这几天总算在google帮助下找到了解决方法. 在~/.android/下找到adb_usb.ini,如果不存在则创建.通过“系统信息”查看到插入的an ...
- hdu1161Eddy's mistakes
Problem Description Eddy usually writes articles ,but he likes mixing the English letter uses, for e ...
- 网络编程之TCP
知识补充:源IP地址和目的IP地址以及源端口号和目的端口号的组合称为套接字.其用于标识客户端请求的服务器和服务. TCP编程的实现步骤:服务器端:1.通过ServletSocket创建绑定到指定客户端 ...
- [hdu5113]Black And White2014北京赛区现场赛B题(搜索加剪枝)
转载请注明出处: http://www.cnblogs.com/fraud/ ——by fraud Black And White Time Limit: 2000/2000 MS ...
- ExtJS 创建动态加载树
Ext 中导航树的创建有两种方式:1.首先将所有的数据读出来,然后绑定到前台页面.2.每点击一个节点展开后加载子节点.在数据量比较小的时候使用第一种方式加载的会快一些,然而当数据量比较大的时候,我还是 ...
- 如何判断CPU的位数
CPU是16位,32位,还是64位,主要指的是数据总线(data bus)有多少位,16位数据总线表示CPU一次可以从内存取2个byte的数据,32位数据总线表示CPU一次可以从内存取4byte数据, ...
- Android 播放gif图片
Android的原生控件并不支持播放GIF格式的图片.我们都知道,在Android中如果想要显示一张图片,可以借助ImageView控件来完成,但是如果将一张GIF图片设置到ImageView里,它只 ...
- Bootstrap 静态分页 和 jquery_pagination插件 动态分页
第一种Bootstrap 实例 - 默认的分页 <!DOCTYPE html> <html> <head> <title>Bootstrap 实例 - ...