接着上一篇Runtime初探,我们来探究一下Runtime的工作原理。
1、runtime的交互
顾名思义,就是OC如何与runtime进行交互。我们看下文档:苹果告诉我们有3种途径交互:
第一种:Objective-C Source Code,通过OC源码,也就是说我们只需要编写OC代码,runtime系统会自动在后台工作。当我们编译OC的类和方法时,编译器会自动创建可以动态调用实现的数据结构和函数。
第二种:NSObject Methods,通过调用NSObject中的提供的方法,包括:
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)respondsToSelector:(SEL)aSelector;
+ (BOOL)instancesRespondToSelector:(SEL)aSelector;
+ (BOOL)conformsToProtocol:(Protocol *)protocol;
- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;
+ (NSString *)description;
...
第三种:Runtime Functions,通过调用Runtime函数,包含在/usr/include/路径下的头文件,所有的API在这篇文档里面都有说明 Objective-C Runtime Reference.
其中,第一种是OC编译器自动生成的,不需要我们参与,第二种相信小伙伴们都用过,第三种可能有的小伙伴跟我一样用的较少,或者说还没有使用过,这个需要自己多花点时间去探索和研究了,后面如果有时间我也会好好研究一下再和大家分享。
2、runtime的消息机制
本章描述如何将消息表达式转换为objc_msgSend函数调用,以及如何通过名称引用方法。然后解释如何利用
objc_msgSend,以及如何-如果需要-可以绕过动态绑定。
一、The objc_msgSend Function
文档里面讲到了 [receiver message] 对象的方法调用编译后实际上是通过给接受者发送消息实现的,即 objc_msgSend(receiver, message), 如果有参数,可以进一步表现为 objc_msgSend(receiver, message, arg1, arg2, ...)。
实际看一下这个是如何实现的:
创建一个OC工程,新建一个Receiver的类,声明并实现一个message方法,在实现中打印这个方法名, 然后在main函数中实例化一个Receiver对象并调用message方法:
((void (*)(id, SEL))(void *)objc_msgSend)((id)receiver, sel_registerName("message"));
简写如下(要引入头文件 ):
objc_msgSend(receiver, @selector(message));
确实如文档所说调用了objc_msgSend方法, 那么我们可以得出结论:方法调用的本质就是给这个对象发送一条消息,消息的内容包括方法名和参数
,这就是runtime的消息机制。(苹果让我们不要主动使用objc_msgSend,因为编译器会自动生成。)
消息传递的关键在于编译器为每个类和对象构建的结构。每个类的结构都包括以下两个基本要素:
* 指向父类的指针。
* 类调度表。此表具有将方法选择器与它们标识的方法的特定的类的地址相关联的条目。`setOrigin::`方法的选择器与其实现的地址相关联,`display`方法的选择器也与其实现的地址相关联,依此类推。
当一个新的对象被创建时,系统将为其分配内存,并初始化其实例变量。第一个变量就是一个指向其类结构的指针,叫做`isa`,这个对象通过`isa`指针可以访问它的类,并通过该类访问它继承的所有类。
讲的已经非常清楚了,编译器在对象被创建并初始化时的第一个变量就是一个指向它的类的结构体的指针,通过这个指针可以访问这个类和所继承的所有父类,这样消息就会向上层层传递,遍历类调度表,直到找到方法名对应的实现并执行。
文档还提到了消息加速的问题,为了提高消息的传递速度,运行时系统在使用时缓存了方法的选择器和地址,它为每个类都创建一个单独的缓存,可以包含继承方法的选择器以及类中定义的方法,当第一次调用方法后,父类或者基类的方法就会被缓存到该对象的类的缓存中,后面在调用时,就不必往上层层传递去寻找,从而提高了消息的传递速度。
二、Using Hidden Arguments
使用隐式的参数,意思很简单,就是runtime在执行objc_msgSend方法时,会把所有的参数都传递过去,其中包含两个隐藏的参数:1、接受者本身,即
self
, 是指向消息接受者的指针;
2、方法选择器,即
_cmd
,是指向方法选择器的指针;
虽然这两个参数没有被现式的定义在方法中,但是源代码仍然可以调用这两个参数,(
self
大家应该都在使用,但是_cmd
可能有小伙伴没使用过,NSStringFromSelector(_cmd)可以获取方法名,或者给对象动态添加属性时也可以用到,有兴趣的小伙伴可以自己研究一下。)
三、Getting a Method Address
获取方法地址,规避动态绑定的唯一方法是获取方法的地址并像调用函数一样直接调用它。这种情况可能适用于极少数情况下,特定方法将连续多次执行,并且您希望每次执行该方法时都避免消息传递的开销。
使用场景:为了节省方法传递的开销,比如在一个for循环中,连续不断的调用同一个方法。
使用方法:NSObject类中定义的方法,methodForSelector:
苹果给出的使用案例:
下面的示例显示了如何`setFilled:`调用实现该方法的过程:
void (* setter) (id,SEL,BOOL);
setter = (void (*) (id,SEL,BOOL))[target methodForSelector:@selector(setFilled :)];
for (int i = 0; i < 1000; i++)
setter(targetList[i],@selector (setFilled:), YES);
传递给过程的前两个参数是接收对象 (self) 和方法选择器 (_cmd), 这是前面提到的两个隐式参数,但在将方法作为函数调用时必须使其显式化。
使用methodForSelector:
规避动态绑定可以节省大部分消息传递的时间。但是,只有在特定消息重复多次的情况下,节省才会显著,如for
上面所示的循环。
注意,methodForSelector:
是由Cocoa运行时系统提供的; 它不是Objective-C语言本身的一个特性。
下一遍继续研究:Runtime的工作原理(二)
觉得对你有用或者喜欢的小伙伴记得给我点个赞,如果有什么问题或者错误欢迎一起探讨和指正。