Runtime之Class结构

2019-06-16  本文已影响0人  coder_feng

通过之前一篇文章Runtime之isa,估计大家对isa的本质都比较了解,现在这篇我们来看一下Class结构的相关内容。

Class 类结构

从源码中我们可以看到Class的结构大概如下:

class主要图 bits.data()

class_rw_t

class_rw_t

class_rw_t 里面的methods,properties,protocols是二维数组,是可读可写的,因此可以动态的添加方法,并且更便于分类方法的添加,包含了类的初始化内容,分类的内容,并且类在relizeClass的时候,将class_ro_t的类初始化信息和分类信息(attachList函数内通过memmove和memcpy两个操作将分类的方法列表合并在本类的方法列表中)组合成class_rw_t类型

rezlizeClass

从上述源码中就可以发现,类的初始信息本来就是存储在class_ro_t中的,并且ro本来是指向cls->data()的,也就是bits.data()得到的是ro,但是在运行的过程中创建了class_rw_t,并将cls->data 指向rw,同时将初始化信息ro赋值给rw中的ro,最后在通过setData(rw)设置data,那么此时bits.data()得到的就是rw,之后再去检查是否有分类,同时将分类的方法,属性,协议列表整合存储在class_rw_t的方法,属性及协议列表中

class_ro_t

class_ro_t

class_ro_t 里面的methodList,baseProtocols,ivars,baseProperties是一维数组,是只读的,包含了类的初始内容

method_t

我们知道method_array_t、property_array_t、protocol_array_t中以method_array_t为例,method_array_t中最终存储的是method_t,method_t是对方法、函数的封装,每一个方法对象就是一个method_t。通过源码看一下method_t的结构体

method_t type Encoding

mehod_t 是对方法的封装,IMP代表函数的具体实现,SEL代表方法函数名,一般叫做选择器,可以通过@selector(),和sel_registerName()获得,可以通过sel_getName()和NSStringFromSelector()转成字符串,types包含了函数返回值,参数编码的字符串。

IMP

debug mode imp 是否指向函数实现

type

type


根据前面type Encoding 图,可以知道这个代表

v:代表返回值void 

16 表示参数的占用空间大小

@:id

0:id后面的0表示从0位开始存储,id 占8位空间

:代表SEL

8:表示从第8位开始存储,SEL同样占8位空间

- (void)personTest;我们知道任何方法都默认有两个参数的,id类型的self,和SEL类型的_cmd,而上述通过对types的分析同时也验证了这个说法。现在我们添加参数再来测试相关情况看看

- (void)personTest:(int)age height:(float)height:

v24@0:8i16f20

v        24    @        0        :    8    i    16    f            20
void            id               SEL        int         float

参数的总占用空间为8 + 8 + 4 + 4 = 24

id 从第0位开始占据8位空间,SEL从第8位开始占据8位空间,int 从第16位开始占据4位空间,float从20位开始占据4位空间

另外iOS提供了@encode的指令,可以将具体的类型转化成字符串编码

@encode

SEL

SEL可以通过@selector()和sel_registerName()获得

@selector()和sel_registerName()

也可以通过sel_getName()和NSStringFromSelector()将SEL转成字符串

sel_getName()和NSStringFromSelector()

不同类中相同名字的方法,所对应的方法选择器是相同的。关于这点,你可以自己打印看看的

NSLog(@"%p,%p", sel1,sel2); 会发现结果是一样的

方法缓存cache_t

cache_t

buckets:散列表;

 _mask:散列表的长度 - 1;

_occupies:已经缓存的方法数量

还没有缓存personTest 缓存了peronTest bucket_t

_key:SEL作为key;_imp:函数的内存地址

其实cache_t cache 是用来缓存曾经用过的方法,可以提高方法的查找速度,前面介绍OC对象的时候,已经说过了,如果一个对象调用方法的时候,需要去方法列表里面进行遍历查找,如果方法列表不存在,就会通过superclass找到父类的类对象,在去父类的类对象方法列表里面查找,一直找到顶层,但是如果方法需要调用很多次的话,那就相当于每次调用都需要去遍历很多方法,这样对性能是有一定损耗的,特别是如果要寻找的方法都是在父类里面的话,整个查找效率就会显得低下了,所以就要有cache_t来进行方法缓存,每当调用方法的时候,会先去cache中查找是否有缓存的方法,如果没有缓存,再去类对象方法列表中查找,以此类推直到找到方法之后,就会将方法直接存储在cache中,下一次在调用这个方法的时候,就会在类对象的cache里面找到这个方法,直接调用了,不会再先之前描述的那样又要重新执行一遍流程


cache_t 缓存原理

从上述的源码中,可以看到bucket列表是一个散列表,也叫哈希表,是根据关键码值(Key Value)而直接进行访问的数据结构,也就是说,它通过关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表。苹果又是怎么设计这个逻辑的呢,通过什么函数能够在列表中快速并且准确的找到对应的key以及函数实现呢?

缓存主要方法

cache_fill及cache_fill_nolock 函数

cache_fill cache_fill_nolock

reallocate函数

reallocate 扩展容量的枚举

expand()函数

当散列表的空间被占用超过3/4的时候,散列表会调用expand()函数进行扩展,看看一下expand()函数内散列表如何进行扩展的

expand

find 函数

find函数是通过散列函数,找到对应的位置,然后找到对应的value,看源码图:

find cache_hash cache_next

cache 总结

当第一次使用方法时,根据运行时的消息机制同isa找到对应的方法之后,会对方法以SEL为key,IMP为value的方式缓存在cache的_buckets中,当第一次存储的时候,会创建具有4个空间的散列表,并将_mask的值赋值为散列表长度减1,然后将SEL&mask计算出来的方法存储的下标值,并将方法存储在散列表中。举例说明,如果计算出的下标值为3,那么就直接将方法直接存储在下标为3的空间中,前面的空间会留空,这个也是cache缓存快的原因,通过空间换时间,另外当散列表中存储的方法占据散列表长度超过3/4的时候,散列表会进行扩容操作,将创建一个新的散列表并且空间扩容至原来空间的两倍,并重置_mask的值,最后释放旧的散列表,此时再有方法需要进行缓存的话,需要重新通过SEL & mask计算出,然后再存储新的,旧的会全部释放掉;另外如果一个类中方法很多,其中很可能会SEL & mask得到的值为同一个下标值,那么会调用cache_next 函数往下减1,然后重新判断,如果下标值-1 位空间有存储方法,并且key没有与存储的key相同,那么继续再到前面一位进行比较,直到找到一位空间没有存储方法key等于0的,或者key与要存储的key相同为止,如果到下标为0的话,就会到下标为_mask的空间重新再查找,可以看看下面两张图:

散列表逻辑存储图

当方法loadData需要缓存的时候,这个时候通过SEL&_mask的值为2,但是此时获取的_buckets[2].key() = studentTest,没有等于0,也没有等于loadData,这个时候会调用cache_next函数重新进行下一次判断,然后到了_buckets[1].key() = 0,因此@selector(loadData)存放在下标为1的空间内。

测试验证环节

这一步,就留给感兴趣的人,自己写代码自己验证哈,如果理解前面所的内容,自己验证应该不难,也可以参考demo来验证,enjoy!

上一篇下一篇

猜你喜欢

热点阅读