我们先来整体的看一下结构
- 1、class类中只要有
isa指针
、superClass
、cache方法缓存
、bits具体的类信息
- 2、
bits & FAST_DATA_MASK
指向一个新的结构体Class_rw_t
,里面包含着methods方法列表
、properties属性列表
、protocols协议列表
、class_ro_t类的初始化信息
等一些类信息
Class_rw_t
Class_rw_t
里面的methods方法列表
、properties属性列表
都是二维数组,是可读可写的,包含类的初始内容
,分类的内容
class_ro_t
class_ro_t
里面的baseMethodList,baseProtocols,Ivars,baseProperties是一维数组,是只读的,包含类的初始化内容
method_t
method_t
是对方法的封装
struct method_t{
SEL name;//函数名
const char *types;//编码(返回值类型,参数类型)
IMP imp;//指向函数的指针(函数地址)
}
IMP
IMP代表函数的具体实现
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector)
SEL
SEL代表方法名,一般叫做选择器,底层结构跟char *
类似
- 可以通过
@selector()
和sel_registerName()
获得 - 可以通过
sel_getName()
和NSStringFromSelector()
转成字符串 - 不同类中相同名字的方法,所对应的方法的选择器是相同的
- 具体实现
typedef struct objc_selector *SEL
types
types包含了函数返回值,参数编码的字符串
结构为:返回值 参数1 参数2...参数N
iOS中提供了一个叫做@encode
的指令,可以将具体的类型表示成字符串编码
例如
// "i24@0:8i16f20"
// 0id 8SEL 16int 20float == 24
- (int)test:(int)age height:(float)height
每一个方法都有两个默认参数self
和_msg
我们可以查到id
类型为@
,SEL
类型为:
- 1、第一个参数
i
返回值 - 2、第二个参数
@
是id 类型的self
- 3、第三个参数
:
是SEL 类型的_msg
- 4、第四个参数
i
是Int age
- 5、第五个参数
f
是float height
其中加载的数字其实是跟所占字节有关
- 1、
24
总共占有多少字节 - 2、
@0
是id 类型的self
的起始位置为0 - 3、
:8
是因为id 类型的self
占字节为8,所以SEL 类型的_msg`的起始位置为8
Class内部结构中有一个方法缓存cache_t
,用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。
cache_t
结构体里面有三个元素
-
buckets
散列表,是一个数组,数组里面的每一个元素就是一个bucket_t
,bucket_t
里面存放两个_key
SEL作为key_imp
函数的内存地址
-
_mask
散列表的长度 -
_occupied
已经缓存的方法数量
为什么会用到方法缓存
这张图片是我们方法产找路径,如果我们的一个类有多个父类,需要调用父类方法,他的查找路径为
- 1、先遍历自己所有的方法
- 2、如果在自己类中找不到方法,则遍历父类所有方法,没有查找到调用方法之前,一直重复该动作
如果每一次方法调用都是走这样的步骤,对于
系统级方法
来说,其实还是比较消耗资源的,为了应对这个情况。出现了方法缓存
,调用过的方法,都放在缓存列表中,下次查找方法的时候,现在缓存中查找,如果缓存中查找不到,然后在执行上面的方法查找流程。
散列表结构
散列表的结构大概就像上面那样,数组的下标是通过@selector(方法名)&_mask
来求得,具体每一个数组的元素是一个结构体,里面包含两个元素_imp
和@selector(方法名)作为的key
我们在上一篇文章中知道,一个值与&上一个_mask
,得出的结果一定小于等于_mask
值,而_mask
值为数组长度-1,所以任何时候,也不会越界。
其实这就是散列表的算法,也有一些其他的算法,取余
,一个值取余
和&
的效果是相同的。
但是这其实是有几个疑虑的
- 1、初始
_mask
是多少? - 初始_mask
我简单了尝试了一下,第一次可能给3 - 2、随着方法的增加,方法数量超过
_mask
值了怎么办 - 随着方法的增多,方法数量肯定会超过_mask
,这个时候会清空缓存散列表,然后_mask
*2 - 3、如果两个值
&_mask
的值相同了怎么办 - 如果两个值&_mask
的值相同时,第二个&
减一,知道找到空值,如果减到0还没有找到空位置,那就放在最大位置 - 4、在没有存放
cach_t
的数组位置怎么处理- 在没有占用是,会在空位置的值为
NULL
- 在没有占用是,会在空位置的值为
源码查看
我们在objc-cache.mm
文件中查找bucket_t * cache_t::find(cache_key_t k, id receiver)
方法。
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
计算index值
mask_t begin = cache_hash(k, m);
这个方式是计算下标的,我们点击进入查看具体实现,就是@selector(方法名)&_mask
当两个值求的下标相同时
(i = cache_next(i, m)) != begin
具体实现为
arm64
和x86
实现方法不一样
这里有一个MJ老师
封装的能够查看对象各种属性的方法,想要使用的可以在这里查看