class的结构
方法缓存
我们先来整体看一下结构
-
- class类中只有isa指针、superClass、cache方法缓存、bits具体的类信息
-
- 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, …);
- id _Nonnull : 第一个参数是指向self的指针. (如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
- SEL _Nonnull : 第二个参数是方法选择器(selector)
- 每一个OC方法,都有两个隐藏参数
- self参数,指向当前实例的指针,用于访问对象的属性和方法。
- 在实例方法中,self指向当前对象实例
- 在类方法中,self指向当前类对象
- _cmd参数
- _cmd是一个SEL类型的参数,表示当前正在执行的方法选择器(方法名)
- 它可用于在运行时获取的当前方法的名称,比如在日志中输出当前方法的名称。
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 : 已经缓存的方法数量
为什么会用到方法缓存
这张图片是我们方法产找路径,如果我们的一个类有多个父类,需要调用父类方法,他的查找路径为
-
- 先遍历自己所有的方法
-
- 如果在自己类中找不到方法,则遍历父类所有方法,没有查找到调用方法之前, 一直重复该动作。如果每一次方法调用都是走这样的步骤, 对于系统级方法来说,其实还是比较消耗资源的,为了应对这个情况。出现了方法缓存,调用过的方法,都放在缓存列表中,下次查找方法的时候,先在缓存中查找,如果缓存中查找不到,然后在执行上面的方法查找流程。
散列表结构
散列表的结构大概就像上面那样,数组的下标是通过@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
面试题
- class_rw_t和class_ro_t结构体中,都有方法列表,属性列表,协议列表,有什么区别呢?
答:
-
方法列表:
- class_ro_t中存储了类的所有静态方法
,包括从父类继承的方法
。这些方法在类的编译时就被确定
下来了。
- class_rw_t中存储了类的动态方法列表
,包括在运行时
添加或修改的方法。 -
属性列表:
- class_ro_t中存储了类的所有静态属性
,包括从父类继承的方法
。这些属性在类的编译时
就被确定下来了。
- class_rw_t中存储了类的动态属性列表
,包括在运行时
添加或修改的属性。 -
协议列表:
- class_ro_t中存储了类遵循的所有静态协议
,这些协议在类的编译时
就被确定下来了。
- class_rw_t中存储了类在运行时动态遵循的协议列表
。 -
讲一下class的结构,class_rw_t的结构,class_ro_t的结构
- 讲一下这三者的关系
class结构中有一个bits,通过&FAST_DATA_MASK得到class_rw_t结构,class_rw_t结构体中一个成员变量const class_ro_t *ro;