一、runtime原理
runtime:简称运行时,因为OC就是运行时机制,最主要的是消息机制。对于C,函数的调用在编译时会决定调用哪个函数。对OC,在编译的时候不能决定真正调用哪个函数,只有在运行时才会根据函数的名字找到对应的函数来调用。
在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行时才会报错,而C调用未实现的函数会报错。
二、isa详解
要想学习Runtime,首先要了解它底层的一些常用数据结构,比如isa指针。
1、在arm64架构之前,isa就是一个普通的指针,存储着Class或者Meta-Class对象的内存地址。
2、从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。
1)nonpointer:0,代表普通的指针,存储着Class、Meta-Class对象的内存地址;1,代表优化过,使用位域存储更多的信息
2)has_assoc:是否有设置过关联对象,如果没有,释放时会更快
3)has_cxx_dtor:是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
4)shiftcls:存储着Class、Meta-Class对象的内存地址信息
5)magic:用于在调试时分辨对象是否未完成初始化
6)weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
7)deallocating:对象是否正在释放
8)extra_rc:里面存储的值是引用计数器减1
9)has_sidetable_rc:引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
三、方法缓存
Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度
我们如果要调用某个方法,会拿方法名去查找散列表中对应的key, 找到key之后就会通过_imp指针去找到函数的内存地址去调用函数。
例如:
在这个散列表中,我们调用personTest方法时,会拿方法名 & _mask得到一个数据,这个数据就是编号。通过编号获取bucket_t的值。假如有个person类,我们需要调用类里面的test方法,具体的一个查看过程大致为:
Class的结构图
结合上图,person会通过isa指针到类结构体中的cache里查看缓存,第一次调用缓存中是没有这个对象的,这时会通过bits去调用_rw_t里面的methods去遍历方法列表。如果找到了test方法就会调用它并把它放到自己的cache里面。下一次调用的时候再次通过isa指针去缓存里面找。
如果test方法不在自身类中而存在父类中。跟前面的查找方法类似,在methods列表中找不到test方法时会通过superClass指针去父类查找,看父类的cache里面是否有,如果没有就去_rw_t中查找。最后如果找到了会调用这个方法,并切把这个方法缓存到person自己的cache中。
四、objc_msgSend消息机制
OC中的方法调用,其实都是转换成objc_msgSend函数的调用。
objc_msgSend的执行流程可以分为3个阶段:
1、消息发送
消息发送的大致流程
2、动态方法解析
objc_msgSend动态方法解析
可以通过动态添加方法来进行方法的调用。使用resolveInstanceMethod或resolveClassMethod来实现。
方法一:
//
- (void)other
{
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 获取其他方法
Method method = class_getInstanceMethod(self, @selector(other));
// 动态添加test方法的实现
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));
// 返回YES代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
方法二:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 获取其他方法
struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
// 动态添加test方法的实现
class_addMethod(self, sel, method->imp, method->types);
// 返回YES代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
如果调用一个没有实现的test的方法,代码中添加了动态方法other,那么就会把方法添加到缓存重新进入到第一阶段进行消息发送。如果没有实现动态方法解析,在调用test时找不到方法的实现,就会进入消息转发阶段。
3、消息转发
消息转发流程
如果调用一个没有实现的test方法,代码中也没有添加动态方法,则会进入消息转发流程。这个流程大致为:
步骤一:
//这个方法是转发SEL去对象内部的其它可以响应该方法的对象。如果返回self或nil,则说明没有可以响应的目标,进入步骤二
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
// objc_msgSend([[MJCat alloc] init], aSelector)
return [[MJCat alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
步骤二:如果方法签名返回的值类型和参数正确,则进入步骤三
方法签名:返回方法的值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
方法签名的作用:它的作用就是验证方法是否正确,因为我们在调用函数(即发送消息)的时候,需要两个东西:1)函数名和参数列表 2)参数类型和返回值类型
函数名和参数列表可以从Selector中获取,但是参数和返回值类型则需要从方法签名中获得。
步骤三:
NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
anInvocation.target 方法调用者
anInvocation.selector 方法名
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// anInvocation.target = [[MJCat alloc] init];
// [anInvocation invoke];
[anInvocation invokeWithTarget:[[MJCat alloc] init]];
}
如果上述三个步骤都找不到处理消息的对象,则会调用doesNotRecognzieSelector抛出异常。
PS:如果是类对象调用一个方法,它的消息发送流程与实例方法调用类似。只不过在消息发送的时候是使用元类对象去执行,而元类对象就是一个特殊的类对象而已,所以流程是一样的。








网友评论