cache_t结构

在objc4源码中,objc_class结构中有一个cache_t的成员变量。

struct objc_class : objc_object {
  // Class ISA;
  Class superclass;
  cache_t cache;       // formerly cache pointer and vtable
  class_data_bits_t bits;  // class_rw_t * plus custom rr/alloc flags
}

cache的作用是在objc_msgSend进程中会先在cache中依据办法名来hash查找办法完成,假如能查找到就直接掉用。假如查找不到然后再去rw_t中查找。然后再在cache中缓存。

struct cache_t {
private:
  explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
  union {
    struct {
      explicit_atomic<mask_t>  _maybeMask;
#if __LP64__
      uint16_t          _flags;
#endif
      uint16_t          _occupied;
    };
    explicit_atomic<preopt_cache_t *> _originalPreoptCache;
  };

能够看到有两个成员变量组成。但从界说中看不出成员变量的含义,咱们需求结合其间一个办法的完成去了解。

insert办法

前面提到在objc_msgSend的时分会在cache中增加办法缓存,而这个办法缓存是经过insert办法来增加的。

iOS八股文(五)class类结构cache_t源码详解

insert办法分为两部分,一部分是真实的刺进,另一部分是hash扩容。咱们先看真实的刺进完成。

insert的刺进

源码提示:

fastpath:大概率履行

slowpath:小概率履行

先看源码:

iOS八股文(五)class类结构cache_t源码详解

b是办法缓存的桶子(哈希表)的指针; capacity是现在桶子现在的总容量,那么m是桶子现在的容量减1,即桶子最大的索引。

iOS八股文(五)class类结构cache_t源码详解

iOS八股文(五)class类结构cache_t源码详解

iOS八股文(五)class类结构cache_t源码详解

其间cache_hash是hash核算index的函数。经过办法名和m核算出index。 能够简略看一看,不做重点,不理解也不妨碍对全体的理解。

iOS八股文(五)class类结构cache_t源码详解
最关键的代码是对两者做了与运算,其间mask肯定是2的n次方减1(低位满是1: 0x11、0x1111这类的),这样相当于是对2的n次方取余了。这样算出的index不会有越界的问题。至于为什么是2的n次方减1,capacity的扩容都是2倍的,初始化的容量也是1左移1位(arm64)或者2位(x86)的值,m = capacity - 1,这个mask就是2的n次方减1了。

剩余是do while的履行,具体注释如下。

iOS八股文(五)class类结构cache_t源码详解
其间,假如hash抵触了,会经过cache_next办法寻觅下一个索引i。

在阅读的时分要留意两个简略混杂的概念:

capacity :容量,容纳才能
occupied :实际占用的容量 例如,一个10L的桶子,装了5L水,那么capacity = 10,occupied = 5.

insert的扩容

在刺进之前,有对backet的容量判别,假如不够的话将会对其扩容。先看源码;

iOS八股文(五)class类结构cache_t源码详解
以上代码分为4个分支3个判别,我在代码上面写了简略易懂的注释。

iOS八股文(五)class类结构cache_t源码详解
其间 INIT_CACHE_SIZE的值在arm64的时分是2,在x86_64的结构下是4。 CACHE_END_MARKER 在arm64下为 0, 在x86_64下为 1。 cache_fill_ratio() 在arm64下为7/8, 在x86_64下位3/4。

总结:

arm64结构下,当现在缓存的巨细+1小于等于桶子的巨细的7/8的时分不扩容,当桶子的巨细小于等于8,并且现在缓存的巨细+1小于等于桶子的巨细的时分也不扩容(桶子小于8的时分存满了才扩容)。
x86_64结构下,当现在缓存的巨细+1,再+1小于等于桶子巨细的3/4的时分不扩容。

代码证实

咱们能够经过c++代码界说class的结构,然后经过桥接的方式来获取cache的结构,从而调查backet里边的缓存情况(代码可留言,邮箱发送)。

@implementation NSObject (OSCacheT)
- (void)printCacheT {
  Class cls = [self class];
  //将类型转换成自界说的源码os_objc_class类型,便利后续操作
  struct os_objc_class *pClass = (__bridge struct os_objc_class *)(cls);
  struct os_cache_t cache = pClass->cache;
  struct os_bucket_t * buckets = cache.buckets();
  struct os_preopt_cache_t origin = cache._originalPreoptCache;
  uintptr_t mask = cache.mask();
  NSLog(@"桶子里缓存办法的个数 = %u, 桶子的长度 = %lu",origin.occupied,mask+1);
  //打印buckets
  for (int i = 0; i < mask + 1; i++ ) {
    SEL sel = buckets[i]._sel;
    IMP imp = buckets[i]._imp;
    NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
  }
  NSLog(@"------\r");
}
@end

办法调用如下:

iOS八股文(五)class类结构cache_t源码详解
打印成果如下(arm64 模拟器环境):

iOS八股文(五)class类结构cache_t源码详解

iOS八股文(五)class类结构cache_t源码详解

iOS八股文(五)class类结构cache_t源码详解

能够看到,分别在method1(4/4),method8(8/8),method22(14/16)的时分进行了扩容。这恰好印证了咱们之前得到的定论。留意环境为arm64 模拟器。

考虑

在扩容的时分,苹果为什么要释放旧的缓存,而不是把旧的放入到新的缓存中呢?

  1. 提高msgSend功率,扩容是发生在msgSend中,假如再做copy操作,会影响消息发送的功率。
  2. 缓存命中概率,每个办法调用的概率在底层规划的时分,都视为是一样的。所以之前缓存的办法,在后面调用的概率和其他办法的概率是一样的。即铲除之前的缓存,不会影响命中概率。
  3. 削减扩容次数,从而提高功率。还是2的衍生,假如及时铲除,能够缓存更多的办法,这样,扩容的概率跟放入新缓存相比更小。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。