美文网首页iOS进阶
07-OC中Runtime方法缓存

07-OC中Runtime方法缓存

作者: 光强_上海 | 来源:发表于2020-06-21 17:03 被阅读0次

OC中Runtime的基本概念:

Runtime是OC中的运行时机制,之所以说OC是一门动态性语言,这也正是因为有Runtime机制,Runtime API底层源码大部分也都是使用c、c++和汇编实现

在前面的学习我们基本都了解到,OC的函数调用最终都转换为Runtime的消息发送机制,也就是调用底层函数objc_msgSend(),然而在使用objc_msgSend()进行消息发送时最为关键的就是要通过isa指针找到对应的类,然后找到对应的方法来发送消息,下面我们就再来了解下isa指针

底层源码查找路径:objc4 搜索struct objc_object -> struct objc_object {} -> isa_t isa -> union isa_t{}

通过底层源码查找,我们发现OC基类对象中的isa指针最终经过优化后,变成了isa_t共用体类型,其中核心源码如下:

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

# if __arm64__
    // 这里我们就只保留__arm64__环境
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
}

核心代码如图:

image

上面结构体内每一个元素的作用如图:

image

我们再来类对象的底层数据结构进行分析

objc_classclass对象结构体

struct objc_class : objc_object {
    // Class ISA;
    
    Class superclass;
    
    // 方法缓存数据,所有的已调用过的方法都缓存在`bucket_t`结构体中
    cache_t cache;
    
    // 存储具体的类信息
    class_data_bits_t bits;
}
image

class_data_bits_t & MASK得到class_rw_t结构体

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    // ro_t中存储的是类初始化的信息
    const class_ro_t *ro;

    // 方法列表:[method_list_t, [method_list_t]]
    method_array_t methods;
    
    // 属性列表:[property_list_t, property_list_t]
    property_array_t properties;
    
    // 协议列表,[protocol_list_t, protocol_list_t]
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}
image

class_ro_tclass初始化数据结构体,此结构体只读

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    // 类名
    const char * name;
    
    // 方法列表:[method_t, method_t]
    method_list_t * baseMethodList;
    
    // 协议列表:[protocol_ref_t, protocol_ref_t]
    protocol_list_t * baseProtocols;
    
    // 成员变量列表:[ivar_t, ivar_t]
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    
    // 属性列表:[property_t, property_t]
    property_list_t *baseProperties;
}
image

method_t:method对象的数据结构

struct method_t {
    // 函数名,类似于char *类型,理解为就是个字符串
    SEL name;
    
    // 函数编码,对应的值就是`TypeEncoding`拼接成的字符串
    const char *types;
    
    // 函数的内存地址,指向函数的指针,代表函数的具体实现
    IMP imp;
}
image

TypeEncoding编码表如图:

image

cache_t方法缓存对象的数据结构:

struct cache_t {

    // 用于存储缓存方法的哈希表(散列表)
    struct bucket_t *_buckets;
    
    // 哈希表的长度 -1
    mask_t _mask;
    
    // 已经缓存的方法数量
    mask_t _occupied;
}

bucket_t缓存列表哈希表:

struct bucket_t {

    // SEL作为key,SEL也就是方法名
    cache_key_t _key;
    
    // 函数的内存地址,也就是指向函数的指针
    IMP _imp;
}
image

下面我们创建一个示例工程来验证下方法缓存,示例代码如下:

Person

@interface Person : NSObject

- (void)run;
- (void)eat;
@end


@implementation Person

- (void)run {
    NSLog(@"%s", __func__);
}

- (void)eat {
    NSLog(@"%s", __func__);
}
@end

main函数:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        Person *person = [[Person alloc] init];
        xx_objc_class *cls = (__bridge xx_objc_class*)[Person class];
        
        [person run];
        NSLog(@"111");
        [person eat];
    }
    return 0;
}

我们查看对应的方法缓存打印如图:

image image image

接下来我们打印查看下_buckets哈希表中的存储的bucket_t,代码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        Person *person = [[Person alloc] init];
        
        xx_objc_class *personClass = (__bridge xx_objc_class*)[Person class];
        
        [person run];
        NSLog(@"111");
        [person eat];
        NSLog(@"----------");
        
        cache_t cache = personClass->cache;
        bucket_t *buckets = cache._buckets;
        for (NSInteger i = 0; i <= cache._mask; i ++) {
            bucket_t bucket = buckets[i];
            NSLog(@"%s--%p", bucket._key, bucket._imp);
        }
    }
    return 0;
}

打印如图:

image

我们在修改代码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        Person *person = [[Person alloc] init];
        xx_objc_class *personClass = (__bridge xx_objc_class*)[Person class];
        
        [person run];
        [person eat];
        [person test];
        [person work];
        NSLog(@"----------");
        
        cache_t cache = personClass->cache;
        bucket_t *buckets = cache._buckets;
        for (NSInteger i = 0; i <= cache._mask; i ++) {
            bucket_t bucket = buckets[i];
            NSLog(@"%s--%p", bucket._key, bucket._imp);
        }
        
        NSLog(@"33");
    }
    return 0;
}

查看打印结果如图1:

image

从图1我们可以看出,当我们执行完work方法后,方法缓存列表的容量变为之前的2倍了,长度为8,这是因为当执行完test函数后,方法缓存的数量就达到了最开始分配的4的长度,所以缓存列表长度扩容了一倍,需要注意:在缓存列表扩容后,就会清除掉之前已经缓存的方法,从新缓存

图2

image

我们从图2的终端打印可以看出,此时缓存列表中只缓存了2个方法,testwork方法,因为执行完test方法后,缓存列表扩容,清除了之前缓存的方法

接下来我们来验证下从方法缓存列表哈希表中查找方法的过程,代码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        Person *person = [[Person alloc] init];
        xx_objc_class *personClass = (__bridge xx_objc_class*)[Person class];
        
        [person run];
        [person eat];
        [person test];
        [person work];
        NSLog(@"----------");
        
        cache_t cache = personClass->cache;
        bucket_t *buckets = cache._buckets;
        
        // 通过`@selector(work) & cache._mask`找到方法对应的下标索引index,然后在`buckets`中取出对应的`bucket_t`
        bucket_t workbucket = buckets[(long long)@selector(work) & cache._mask];
        NSLog(@"从哈希表中找方法:%s--%p", workbucket._key, workbucket._imp);
        NSLog(@"----------");
        
        for (NSInteger i = 0; i <= cache._mask; i ++) {
            bucket_t bucket = buckets[i];
            NSLog(@"%s--%p", bucket._key, bucket._imp);
        }
        NSLog(@"33");
    }
    return 0;
}

我们通过方法名work&mask得到一个索引,这个索引便是存储方法work在哈希表中的索引值,通过下图打印方法work的函数地址也可以看出,@selector(work)&mask取出的方法正好就是缓存列表中的work方法

image

我们在看一个继承关系中,方法调用时的缓存策略,当子类调用父类的方法时,这时方法是缓存在子类中还是缓存在父类中尼,代码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
    
        Student *student = [[Student alloc] init];
        xx_objc_class *studentClass = (__bridge xx_objc_class*)[Student class];
        [student run];

        cache_t cache2 = studentClass->cache;
        bucket_t *buckets2 = cache2._buckets;
        
        for (NSInteger i = 0; i <= cache2._mask; i ++) {
            bucket_t bucket = buckets2[i];
            NSLog(@"%s--%p", bucket._key, bucket._imp);
        }
        
        NSLog(@"--------------");
        
        Person *person = [[Person alloc] init];
        xx_objc_class *personClass = (__bridge xx_objc_class*)[Person class];
        cache_t cache = personClass->cache;
        bucket_t *buckets = cache._buckets;

        for (NSInteger i = 0; i <= cache._mask; i ++) {
            bucket_t bucket = buckets[i];
            NSLog(@"%s--%p", bucket._key, bucket._imp);
        }
        
        NSLog(@"---");
    }
    return 0;
}
image

我们从上图的打印可以得出结论,当子类调用父类的方法时,这时这个方法只会缓存在调用者的类对象中,也就是说只会缓存在子类中,并不会缓存到父类对象中

OC方法缓存中哈希表(散列表)设计原理如图:

image

讲解示例Demo地址:https://github.com/guangqiang-liu/07-RumtimeMethodCache

更多文章

相关文章

  • 07-OC中Runtime方法缓存

    OC中Runtime的基本概念: Runtime是OC中的运行时机制,之所以说OC是一门动态性语言,这也正是因为有...

  • 温故而知新-ObjC Runtime 方法缓存

    简介 ObjC Runtime 的消息传递过程中,会使用方法缓存提高效率。 本文主要是记录方法缓存的几个特点: 每...

  • hook forwardingTargetForSelector

    方法调用流程 runtime中具体的方法调用流程大致如下: 1.首先,在相应操作的对象中的缓存方法列表中找调用的方...

  • Runtime:方法缓存

    目录一,objc_class二,class_ro_t和class_rw_t三,method_t四,方法缓存原理五,...

  • Runtime 02 - Class(方法信息列表、方法缓存)

    Runtime 02 - Class(方法信息列表、方法缓存) Class 结构图 关于对 objc_class、...

  • Runtime之方法缓存

    我们都知道,在Objective-c里面,调用一个方法,其实在runtime层的时候会翻译成 可以想象一下,在继承...

  • Runtime 之 方法缓存

    Runtime 之 消息发送流程解析这一节已经介绍了消息发送的整个流程,过程中涉及到的方法缓存的插入,将在本节进行...

  • 面试问题总结

    1.runtime的缓存机制是什么?答案:根据runtime类的数据结构。调用某个方法,首先通过isa和super...

  • Runtime主要用处

    深入理解Objective-C Runtime机制 一:消息转发流程和机制 1、实例方法:本类的缓存方法列表->本...

  • iOS 消息转发流程

    runtime方法查找流程及消息转发 方法查找 方法查找的流程:缓存查找-->当前类查找-->父类逐级查找 1.缓...

网友评论

    本文标题:07-OC中Runtime方法缓存

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