iOS底层-类的三顾茅庐(三)
2023-10-13 20:04:07
上文讲解完了类对象的结构体objc_class
用来存储类信息的成员bits
,整个结构还剩下方法的缓存cache
,放在压轴来讲解。
缓存有这么几个使用场景。
-
iOS5.0之前,为了加速访问方法,方法的实现函数的指针会被缓存起来。
-
runtime的一些方法,比如:
class_getMethodImplementation
、object_copy()
、objc_msgsend
这些都需要先找到方法的实现,如果通过反射调用,那么需要首先查找实现函数的指针,这些方法都会去查找方法缓存,方法缓存找到了实现函数指针后,就可以直接调用。 -
实现了动态特性
@dynamic
或@synthesize
的属性,编译器并不会帮我们生成set、get方法,当我们调用这些属性的时候,也是通过查找缓存的方式来找到set、get方法的实现。
说完了场景,我们来看看缓存的实现。
struct objc_cache
是一个散列表,包含256个指针,每个指针指向一个链表。
链表的结点叫做objc_method
,结构定义如下:
struct objc_method {
SEL _sel;
char *_types;
IMP _imp;
}
_sel
是selector,_types
是参数类型编码,_imp
是方法的实现。
当需要获取方法的实现的时候,流程如下:
-
根据方法的selector和参数类型,计算一个哈希值。
-
利用哈希值,找到对应的链表。
-
在链表中,找到匹配方法的selector和参数类型的结点。
-
返回结点中的方法实现。
static IMP _method_lookup_internal(Class cls, SEL sel, const char *types) {
uintptr_t mask = _objc_selector_mask(sel);
// 这里是一连串的宏展开
struct objc_cache *cache = cls->_cache;
// 这里又是好几层宏展开
uintptr_t bit = 1 << ((mask >> cls->_shift) & 0xff);
uintptr_t index = cls->_cache_size - ((mask >> cls->_shift) >> 8);
struct objc_method *ent = cache[index];
struct objc_method *result = NULL;
while (ent) {
if (ent->_sel == sel && ((bit & ent->_bits) ||
(types && strcmp(types, ent->_types) == 0))) {
result = ent;
break;
}
ent = ent->_next;
}
return result ? result->_imp : NULL;
}
这里面有个宏_objc_selector_mask
需要简单解释一下。
在iOS7.0之后,SEL
结构变成了32位,这样会给缓存造成太大的存储空间浪费。
所以就加入一个移位操作,比如一个sel是_list
,它的hash是2049773298。
那么_objc_selector_mask
得到的mask
值为1968068398,移动了18个位,丢失了高位信息。
加上高位丢失导致的哈希碰撞,然后加上iOS5.0之前的缓存机制,iOS7.0之前的缓存已经彻底崩溃了。
iOS7.0后,采用新的哈希算法和新的缓存机制后,这些问题被解决了。
_objc_selector_mask
函数如下:
static inline uintptr_t _objc_selector_mask(SEL sel) {
if (objc_debug_selector_mask) {
// 这个宏没有什么用
return *(NSUInteger *)sel;
} else {
struct objc_selector *objc_sel = (struct objc_selector *)sel;
uintptr_t mask = objc_sel->hash >> objc_sel->log2_mask;
objc_sel->log2_mask = 19;
objc_sel->hash &= ((1 << objc_sel->log2_mask) - 1);
return mask;
}
}
下面这张图是iOS7.0之后缓存的设计:
我们可以清晰地看到cache
中是存储objc_method
结构的链表,当缓存命中之后,我们会找到一个objc_method
的结构体,存储了sel
、type
、imp
三个信息。
iOS7.0之后的缓存机制通过mask
和shift
来计算出链表的索引,直接访问链表就完成了一次查找。
在iOS7.0之后,缓存的命中率大幅提高,从而提高了方法调用的性能。