Runtime 一: OC 方法的底层数据结构和缓存机制

今天研究一下 OC 中方法的底层实现原理,在研究method之前,我们先搞清楚Class的底层数据结构.
先用一张图说明类的底层数据结构,然后我们在从runtime源码中验证:

类的底层数据结构图

我们在runtime源码中搜索struct objc_class {知道类的底层数据结构主要如下:

struct objc_class {
    Class ISA;
    Class superclass;
    cache_t cache; // 方法缓存
    class_data_bits_t bits; // 获取具体的类信息
}

class_data_bits_t bits中存储具体的类的信息,在class_data_bits_t结构体内部仔细查找,发现有这么一句代码:

class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

class_rw_t是可读可写的表,里面存储着类和分类的信息:

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;//只读表,存储类原始信息
    method_array_t methods;//方法列表
    property_array_t properties;//属性列表
    protocol_array_t protocols;//协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
}

class_ro_t是只读表,里面存储着类的原始信息:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;//instance对象占用的内存空间,class_getInstanceSize
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;//类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;//成员变量
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

实际上,一开始的时候,类中是没有class_rw_t的,是runtime后来创建的,这一点我们也可以从源码中找到:

class_rw_t 的创建

现在结合rutime源码和截图,我们总结一下class底层结构关系:

  • 1: Class底层结构体主要有4个成员变量:isa , superClass , catche , bits.
  • 2:catche中存储的调用过的方法的缓存,这个我们下面会讲.
  • 3:bits & FAST_DATA_MASK会得到一个可读可写的数据表:class_rw_t,class_rw_t用来存储类原始信息和分类附加的信息.需要注意的是,class_rw_t这个表一开始是不存在的,后来需要的时候才创建的.
  • 4:class_ro_t是只可读的数据表,它里面存储着类原始的信息

OC方法的调用顺序是如果是调用实例方法,就通过实例对象的isa指针找到类对象,从类对象的方法列表中查找,如果如果没找到,在通过superClass从父类的方法列表中查找,这样一层一层往上找;如果是调用类方法,就通过类对象的isa找到元类,从元类的方法列表中查找,如果没找到再打通过superClass到元类的父类中查找...
但是我们想想,如果一个方法调用的很频繁,难道每次都要通过这种方式一遍遍查找吗?显然这种方式是低效的,runtime采用了一种更高效的方式来处理这种情况:如果方法第一次被调用后,会缓存到 cache 中,下次再调用的时候直接从 cache 中查找.
cache的底层结构如下:

cache 底层结构

bucket_t的底层结构如下:
bucket_t 底层结构

buckets就是一个数组,里面存放着一个一个的bucket_t:
bucekts 数组

那么runtime是如何从cache中查找方法的呢?难道也是遍历buckets数组吗?肯定不是,遍历数组的那不就跟没优化一样吗?cache的工作原理是:采用散列表的方式把方法插入到buckets时,会用 SEL & _mask得到一个索引值,直接把bucket_t插入到索引值所在的位置.
这样的话就不用每次一个个遍历去查找方法,效率很高.但是这样会有个问题:SEL & _mask 得到索引值并不是按顺序的,他是无序的.比如说:如果 SEL & _mask = 20,那么前面 19 个内存单元就要置为 null 了,这就是 散列表的弊端,牺牲空间换时间.
这种方式虽然效率大大提升了,但是会有个弊端:如果两个 SEL 按位与 _mask 得到的索引相同怎么办?这种情况是很可能发生的.我们从runtime源代码中看看是怎么处理这种情况的.
cache_t结构体中有一个struct bucket_t * find(cache_key_t key, id receiver)方法,这个方法里面就是从buckets查找方法的逻辑:
find 方法

继续进入chche_next():
chache_next() 方法

从上图可以看到,如果索引相等,从buckets中找到bucket_t,然后取出bucket_t中的key和传入的key判断,如果两个key不相等,在arm64环境下,会先把i - 1后再&_mask得到一个新的索引,继续查找,直到找到两个key相等位置.
现在我们已经知道了Class的底层数据机构以及runtime是如何存储和查找方法的.下面我们将研究一下method_t:
method_t 在 class_rw_t 中的位置

method_t 结构体

  • SEL: 方法名,函数名,一般叫做选择器,底层结构跟 char *类似
    · 可以通过@selectorsel_registerName()获得.
    · 可以通过sel_getName()NSStringFromSelector()转成字符串.
    · 不同类中相同名字的方法,所对应的方法选择器是相同的.
  • IMP: 函数的具体实现
  • types: 包含了函数的返回值类型,参数类型编码的字符串
    例如我们随便声明一个函数- (void)test;,他的types就是v16@0:8.代表的意思是:
    types 解释图

    有人可能会觉得奇怪,- (void)test方法并没有参数呀,为什么types会多出两个参数?
    实际上,每一个OC方法都会默认有两个参数,比如说- (void)test的完整形式就是:- (void)test:(id self SEL sel),这也就是为什么我们能在每个方法中调用self,其实是方法参数.至于为什么voidv表示,id@,这都是苹果规定的,在苹果官方文档中有对照表:
    对照表

另外iOS中还提供了一个@encode()指令,可以将具体类型转换成字符串编码.

encode

总结:

  • OC 方法在第一次调用后,会被添加到cache_t缓存中,下次调用时直接从缓存中查找.
  • cache_t中有三个成员变量buckets,_mask,_occupied:
    buckets是一个数组,存放着一个个的bucket_t.
    _maskbuckets数组的数量减 1,外部传入的 SEL & _mask得到buckets数组中的索引(下标).
    _occupied:已经缓存的方法.
  • bucket_t有两个成员key,imp
    key就是SEL;imp就是方法的实现地址.
  • OC 方法的底层是method_t 结构体,主要有三个成员:
    name:函数名称;
    types:编码,(函数返回值类型和参数类型);
    imp:函数地址

验证:
上面讲的都是从源码中推测出来的理论,实际上是不是这样呢?我们自己敲代码验证一番.
我们创建3个类Son , Mother , Person,他们之间的继承关系是:Son : Mother : Person,这3个类中都有一个- (void)personTest;方法.

方法调用之前

方法调用之后

_buckets 扩容:
现在我们更改一下代码,创建一个Son的实例对象son,分别调用Son , Mother , Persontest方法:

扩容之前

我们过掉断点看看会发生什么:
扩容之后

从结果中我们可以看到_mask的数量从4变成了8,并且_buckets中之前缓存的方法也没有了,只缓存了一个方法personTest.
这是由于_buckets的扩容机制造成的.我们在objc-cache.mm中查找void cache_t::expand()方法:
expand()

我们在进入reallocate方法:
reallocate 重新分配内存

OK,通过上面两张图我们知道了buckets是如何扩展容量的:如果 buckets 的容量不够用了,就直接用旧容量 乘以 2 ,重新分配内存空间.并且把旧的缓存方法都清除.

你可能感兴趣的:(Runtime 一: OC 方法的底层数据结构和缓存机制)