美文网首页
iOS runtime理解

iOS runtime理解

作者: 小豆豆苗 | 来源:发表于2020-02-22 00:26 被阅读0次

一、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:如果是类对象调用一个方法,它的消息发送流程与实例方法调用类似。只不过在消息发送的时候是使用元类对象去执行,而元类对象就是一个特殊的类对象而已,所以流程是一样的。

相关文章

网友评论

      本文标题:iOS runtime理解

      本文链接:https://www.haomeiwen.com/subject/kosafhtx.html