一:Objective-C Runtime 是什么
什么是Objective-C Runtime,在这里举个例子,同是也证明一下Objective-C Runtime与我们平常开发息息相关;比如给button添加点击事件的时候的Selector:
为什么acton参数传递的是一个Selector,而不是一个函数名呢?它与函数和方法有什么不同?
从面向对象角度,为调用Dog对象的eat方法;从Objective-C Runtime是发送消息
二:objc_msgSend
上面对象调用方法编译器会将它转化为一个函数调用
objc_msgSend是Objective-C Runtime中的函数,定义在头文件中
总结:其实所有通过一对方括号进行调用的方法,都是通过Objective-C Runtime的objc_msgSend函数发送的一个消息传递。
那objc_msgSend这个函数到底做了什么?
在这之前我们应该了解一下Runtime中关于类的定义,在上述头文件中有一下结构
person类中所定义的属性和方法,在内存中的存储方式就是通过Runtime的struct objc_class结构来定义,每一个类的实例在Runtime中都会用objc_class这个结构来表示,这也意味着所有对象也都包含了objc_class结构中所定义的属性。
isa的类型,就是objc_class这个结构的类型。isa所指向的结构正是这个类的属性和方法的定义;
再回到objc_msgSend函数中,它的第一个参数就是我们要发送消息的实例。
首先objc_msgSend函数会检测这个实例的isa属性,找到isa中定义的:
methodLists属性表示当前实例的方法列表,它是一个objc_method_list类型的结构:
method_count属性表示当前这个实例中方法的个数;
method_list的每一个元素又是一个objc_method类型的结构:
method_name是SEL类型,就是@selector(themeCK)这样的表达式所表示的类型;Selector,它其实是Runtime的一个数据结构,它代表一个方法的唯一标识。
第二个参数method_types,这个属性用一个字符串表示方法返回值类型以及每个参数的类型,并用@encode规则进行编码。
最后一个参数IMP类型,它表示这个Selector对应的函数的地址,真正要执行的函数地址。
总结:Objective-C中定义的所有类的方法在底层实现上就是一个函数。
三:消息分发流程
说了这么多再来回顾一下objc_msgSend函数:
解释:
消息分发的基本流程图:
四:方法实现
Objective-C中所有的方法调用,其实都会隐式的传递进来两个参数,在开发过程中Objective-C Runtime替我们完成了(1)self (2)_cmd,_cmd表示当前函数所对应的Selector。
五:消息缓存
实际上Objective-C 中调用一个方法需要两个过程,(1)通过消息分发找到对应的函数(注意这里是函数,在Runtime中只有函数Function)(2)然后再调用这个函数,并传递相应的参数。
实际上消息分发的过程是比较消耗性能的,需要进行一系列的查表操作,所以Objective-C Runtime对消息的分发建立了缓存机制。回顾一下objc_class结构的定义,是否还记得它定义了一个cache属性:
它的类型是objc_cache继续找到这个结构的定义:
实际上objc_cache维护了一个哈希表,使用Selector作为键,存储了缓存的函数列表。
六:消息分发的传递机制
比如我们在发送一个消息时,如果当前实例的方法列表没找到对应的函数怎么办,比如发送如下消息:
person类中确实没有定义description方法。对于这样的情况,Objective-C Runtime会继续查找它的父类,使用定义在objc_class结构中的super_class属性。
如果遍历完整个磊曾依然找不到对应的方法实现,默认情况下就会抛出异常:
不过这只是默认行为,其实这个异常可以不抛出的。我们完全在发送了一个并没有实现的消息的时候不让程序崩溃。这就是消息转发机制了。
七:消息转发机制
尽量避免这样的消息发送导致APP崩溃,比如可以使用respondsToSelector:方法在调用之前判断这个实例们能不能相应这个消息 。
在Runtime的整个消息传递中,我们还能在其他时机上处理这个事情,这就是Runtime的消息转发机制。
继承自NSObject的类可以覆盖这个方法:
同时还需要覆盖:
如果person不能相应sayHello这个消息,程序并不会马上崩溃,在这之前Runtime会调用person的forwardInvocation:方法。
forwardInvocation:接受一个类型为NSinvocaton的参数。NSinvocaton中存储了我们发送失败sayHello消息的详细信息。在这里我们把它转发到另一个实例上面。
除了可以通过forwardInvocation:来处理装法规则
,同样还可以忽略掉出错的消息。
八:直接发送消息
加入头文件
message.h中定义的objc_msgSend函数,并没有明确参数列表和返回值类型,所以我们需要强制转换一下,否则我们会遇到编译错误:
然后调用这个转换后的函数,并传入相应的参数:
这样编译通过,输出hello。
九:直接调用函数
我们通过调用objc_msgSend来发送消息,但它还是要经过消息分发的过程。这里绕过消息分发过程直接调用函数。
NSObject中定义了一个methodForSelector方法,可以得到Selector所对应的函数:
我们通过methodForSelector得到了sayHello函数的地址引用,这样就直接调用sayHello函数了,就绕过了Runtime的消息分发机制。