返回

iOS底层-类的三顾茅庐(三)

IOS

上文讲解完了类对象的结构体objc_class用来存储类信息的成员bits,整个结构还剩下方法的缓存cache,放在压轴来讲解。

缓存有这么几个使用场景。

  1. iOS5.0之前,为了加速访问方法,方法的实现函数的指针会被缓存起来。

  2. runtime的一些方法,比如:class_getMethodImplementationobject_copy()objc_msgsend 这些都需要先找到方法的实现,如果通过反射调用,那么需要首先查找实现函数的指针,这些方法都会去查找方法缓存,方法缓存找到了实现函数指针后,就可以直接调用。

  3. 实现了动态特性@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是方法的实现。

当需要获取方法的实现的时候,流程如下:

  1. 根据方法的selector和参数类型,计算一个哈希值。

  2. 利用哈希值,找到对应的链表。

  3. 在链表中,找到匹配方法的selector和参数类型的结点。

  4. 返回结点中的方法实现。

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之后缓存的设计:

image

我们可以清晰地看到cache中是存储objc_method结构的链表,当缓存命中之后,我们会找到一个objc_method的结构体,存储了seltypeimp三个信息。

iOS7.0之后的缓存机制通过maskshift来计算出链表的索引,直接访问链表就完成了一次查找。

在iOS7.0之后,缓存的命中率大幅提高,从而提高了方法调用的性能。