类的cache_t分析

235 阅读13分钟

引言:cache是iOS开发中很重要的一块知识,它为软件的运行节省了大量的内存空间,让我们得以实现流畅丝滑的使用体验,到底cache是什么,为什么它的作用这么大,那么,让我们接下来去认识下OC类中这个十分强大的家伙!

一、cache的数据结构

1、cache结构图

类的cache_t结构图.png

如图所示,cache_t是存在于类中的很重要的东西,作用是缓存一些方法,存储的实体为preopt_cache_entry_t,了解了基本的结构之后,我们就使用LLDB来分析下cache_t的存储方法的结构是什么样的!

二、使用LLDB分析cache

1、首先,创建一个带有方法的类,在其他地方通过调用这些类,获取缓存列表。

image.png image.png

2、通过LGPerson类,内存平移16字节,找到成员cache_t,打印cache_t的结构。


(lldb) p/x pClass
(Class) $0 = 0x0000000100008428 LGPerson
(lldb) p (cache_t *)0x0000000100008438
(cache_t *) $1 = 0x0000000100008438
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298437472
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32808               //方法标签
      _occupied = 0                //存储方法的数量
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802800000000
      }
    }
  }
}

3、由于我们没有执行类相关的方法,上述_occupied数量为0,下面我们执行方法之后,查看cache_t内部的值的存储情况。


(lldb) p/x pClass
(Class) $0 = 0x0000000100008438 LGPerson

(lldb) p (cache_t *)0x0000000100008448
(cache_t *) $1 = 0x0000000100008448

(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4311969968
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 3
        }
      }
      _flags = 32808
      _occupied = 2
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0002802800000003
      }
    }
  }
}

(lldb) p $1->buckets()
(bucket_t *) $3 = 0x00000001010370b0

(lldb) p $1->buckets()[1]
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 47144
    }
  }
}

(lldb) p $4.sel()
(SEL) $5 = "saySomething"

(lldb) p $4.imp(nil,pClass)
(IMP) $6 = 0x0000000100003c10 (KCObjcBuild`-[LGPerson saySomething])

cacht_t结构.png

经过简化,cache_t的结构如上图所示,我们可以通过cache——>_buckets——>bucket的方式,取出类中缓存的方法。 但是,使用LLDB是一个繁复的过程,面临以下这些问题:
① 加了断点之后,源码无法进行调试
② LLDB调试容易丢失之前的符号,容易出错
③ LLDB只能小规模取样,取样出错时可能打印未知的地址值或异常
鉴于以上的问题,我们决定从runtime源码中,抽取cache_t的数据结构,下面让我们进行分析!

三、从runtime源码中抽取cache结构

1、抽取主体部分,cache_t和相关的结构体

//这里为了区分与系统的区别,我们加前缀kc区分

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct kc_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct kc_cache_t {
    struct kc_bucket_t *_buckets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

struct kc_class_data_bits_t {
    uintptr_t bits;
};

// cache class
struct kc_objc_class {
    Class isa;
    Class superclass;
    struct kc_cache_t cache;             // formerly cache pointer and vtable
    struct kc_class_data_bits_t bits;
};

2、根据类的执行,打印类中缓存的方法和存储的地址


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];  // objc_clas
        [p say1];
        [p say2];
        [p say3];
        [p say4];
        [p say1];
        [p say2];
        [p say3];

        //class_data_bits_t
         
        [pClass sayHappy];
        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);

        for (mask_t i = 0; i<kc_class->cache._maybeMask; i++) {
            struct kc_bucket_t bucket = kc_class->cache._buckets[i];
            NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
        }
        NSLog(@"Hello, World!");
    }
    return 0;
}

3、执行不同的方法调用,经过打印,我们得到以下结果


2021-06-25 16:28:23.386238+0800 003-cache_t脱离源码环境分析[73400:477654] LGPerson say : -[LGPerson say1]
2021-06-25 16:28:23.386550+0800 003-cache_t脱离源码环境分析[73400:477654] LGPerson say : -[LGPerson say2]
2021-06-25 16:28:23.386580+0800 003-cache_t脱离源码环境分析[73400:477654] LGPerson say : -[LGPerson say3]
2021-06-25 16:28:23.386599+0800 003-cache_t脱离源码环境分析[73400:477654] LGPerson say : -[LGPerson say4]
2021-06-25 16:28:23.386617+0800 003-cache_t脱离源码环境分析[73400:477654] LGPerson say : -[LGPerson say1]
2021-06-25 16:28:23.386633+0800 003-cache_t脱离源码环境分析[73400:477654] LGPerson say : -[LGPerson say2]
2021-06-25 16:28:23.386654+0800 003-cache_t脱离源码环境分析[73400:477654] LGPerson say : -[LGPerson say3]
2021-06-25 16:28:23.386681+0800 003-cache_t脱离源码环境分析[73400:477654] LGPerson say : +[LGPerson sayHappy]
2021-06-25 16:28:23.386709+0800 003-cache_t脱离源码环境分析[73400:477654] 4 - 7
2021-06-25 16:28:23.386805+0800 003-cache_t脱离源码环境分析[73400:477654] say4 - 0xb840f
2021-06-25 16:28:23.386900+0800 003-cache_t脱离源码环境分析[73400:477654] say1 - 0xb9b0f
2021-06-25 16:28:23.386946+0800 003-cache_t脱离源码环境分析[73400:477654] say3 - 0xb810f
2021-06-25 16:28:23.386970+0800 003-cache_t脱离源码环境分析[73400:477654] (null) - 0x0f
2021-06-25 16:28:23.386988+0800 003-cache_t脱离源码环境分析[73400:477654] (null) - 0x0f
2021-06-25 16:28:23.387006+0800 003-cache_t脱离源码环境分析[73400:477654] say2 - 0xb9e0f
2021-06-25 16:28:23.387028+0800 003-cache_t脱离源码环境分析[73400:477654] (null) - 0x0f
2021-06-25 16:28:23.387059+0800 003-cache_t脱离源码环境分析[73400:477654] Hello, World!
Program ended with exit code: 0

2021-06-25 17:57:59.519572+0800 003-cache_t脱离源码环境分析[23089:602553] LGPerson say : -[LGPerson say1]
2021-06-25 17:57:59.519996+0800 003-cache_t脱离源码环境分析[23089:602553] LGPerson say : -[LGPerson say2]
2021-06-25 17:57:59.520042+0800 003-cache_t脱离源码环境分析[23089:602553] LGPerson say : -[LGPerson say3]
2021-06-25 17:57:59.520076+0800 003-cache_t脱离源码环境分析[23089:602553] LGPerson say : +[LGPerson sayHappy]
2021-06-25 17:57:59.520108+0800 003-cache_t脱离源码环境分析[23089:602553] 1 - 7
2021-06-25 17:57:59.520146+0800 003-cache_t脱离源码环境分析[23089:602553] (null) - 0x0f
2021-06-25 17:57:59.520173+0800 003-cache_t脱离源码环境分析[23089:602553] (null) - 0x0f
2021-06-25 17:57:59.520254+0800 003-cache_t脱离源码环境分析[23089:602553] say3 - 0xb9e8f
2021-06-25 17:57:59.520289+0800 003-cache_t脱离源码环境分析[23089:602553] (null) - 0x0f
2021-06-25 17:57:59.520319+0800 003-cache_t脱离源码环境分析[23089:602553] (null) - 0x0f
2021-06-25 17:57:59.520341+0800 003-cache_t脱离源码环境分析[23089:602553] (null) - 0x0f
2021-06-25 17:57:59.520357+0800 003-cache_t脱离源码环境分析[23089:602553] (null) - 0x0f
2021-06-25 17:57:59.520401+0800 003-cache_t脱离源码环境分析[23089:602553] Hello, World!
Program ended with exit code: 0

2021-06-25 18:00:05.318158+0800 003-cache_t脱离源码环境分析[24228:605764] LGPerson say : -[LGPerson say1]
2021-06-25 18:00:05.318469+0800 003-cache_t脱离源码环境分析[24228:605764] LGPerson say : -[LGPerson say2]
2021-06-25 18:00:05.318506+0800 003-cache_t脱离源码环境分析[24228:605764] LGPerson say : -[LGPerson say3]
2021-06-25 18:00:05.318528+0800 003-cache_t脱离源码环境分析[24228:605764] LGPerson say : -[LGPerson say4]
2021-06-25 18:00:05.318545+0800 003-cache_t脱离源码环境分析[24228:605764] LGPerson say : -[LGPerson say1]
2021-06-25 18:00:05.318562+0800 003-cache_t脱离源码环境分析[24228:605764] LGPerson say : +[LGPerson sayHappy]
2021-06-25 18:00:05.318580+0800 003-cache_t脱离源码环境分析[24228:605764] 3 - 7
2021-06-25 18:00:05.318642+0800 003-cache_t脱离源码环境分析[24228:605764] say4 - 0xb840f
2021-06-25 18:00:05.318678+0800 003-cache_t脱离源码环境分析[24228:605764] say1 - 0xb9b0f
2021-06-25 18:00:05.318707+0800 003-cache_t脱离源码环境分析[24228:605764] say3 - 0xb810f
2021-06-25 18:00:05.318734+0800 003-cache_t脱离源码环境分析[24228:605764] (null) - 0x0f
2021-06-25 18:00:05.326152+0800 003-cache_t脱离源码环境分析[24228:605764] (null) - 0x0f
2021-06-25 18:00:05.326181+0800 003-cache_t脱离源码环境分析[24228:605764] (null) - 0x0f
2021-06-25 18:00:05.326199+0800 003-cache_t脱离源码环境分析[24228:605764] (null) - 0x0f
2021-06-25 18:00:05.326216+0800 003-cache_t脱离源码环境分析[24228:605764] Hello, World!
Program ended with exit code: 0

四、cache_t 的源码探索

cache_fill_nolock的写入过程:

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

首先 cacheUpdateLock.assertLocked(); 访问底层cache时,先锁起来因为这个方法会很频繁进行调用,所以避免访问过程中出现混乱先对当前的操作lock起来.

if (cache_getImp(cls, sel)) return 进行下判断当前的方法有没有之前被缓存过.

*cache_t cache = getCache(cls); cache_key_t key = getKey(sel);

这不操作是获取当前类的缓存,将sel 强转成cache_key_t 类型的key,

mask_t newOccupied = cache->occupied() + 1;

 cache->occupied() 获取当前类中已经存储的方法个数,因为现在正在执行存储的操作所以 newOccupied =  cache->occupied() + 1

mask_t capacity = cache->capacity();

获取当前类的存储空间.

下一步就要进行缓存了这又分为了三种情况:

4.1 当前这个类之前没有进行存储也就是说当前代码刚刚走到调用第一个实例方法的时候. (cache->occupied() = 0 , ****cache->capacity() = 0) cache->isConstantEmptyCache() 判断为YES,进入到  cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);的流程。

因为 cache->capacity() = 0,所以 newCapacity = INIT_CACHE_SIZE  (1 << INIT_CACHE_SIZE_LOG2) 也就是等于4

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();

    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}


bool cache_t::canBeFreed(){  return !isConstantEmptyCache();}


第一个判断就是判断是否是类第一次进行存储,如果是第一次存储 freeOld 会返回false,否则返回yes。

*bucket_t oldBuckets = buckets();

*bucket_t newBuckets = allocateBuckets(newCapacity);

这个就是获取旧的缓存池和设置新的缓存池(设置缓存池的空间有多少)

setBucketsAndMask(newBuckets, newCapacity - 1);

  在这里设置缓存池里的 mask  为 缓存空间 - 1,所以当第一个方法存储完之后 mask会为3 ,这和上面lldb所打印的正好是吻合的,

*bucket_t bucket = cache->find(key, receiver);

if (bucket->key() == 0) cache->incrementOccupied();

bucket->set(key, imp);

最后通过 cache 哈希算法找到buckets(缓存池里)找到缓存空间里找到最适合的bucket  ,将方法实现imp和key 关联起来.

关于这方法的具体实现如下:

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    // 通过cache_hash函数【begin  = k & m】计算出key值 k 对应的 index值 begin,用来记录查询起始索引
    mask_t begin = cache_hash(k, m);
    // begin 赋值给 i,用于切换索引
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            //用这个i从散列表取值,如果取出来的bucket_t的 key = k,则查询成功,返回该bucket_t,
            //如果key = 0,说明在索引i的位置上还没有缓存过方法,同样需要返回该bucket_t,用于中止缓存查询。
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
    
    // 这一步其实相当于 i = i-1,回到上面do循环里面,相当于查找散列表上一个单元格里面的元素,再次进行key值 k的比较,
    //当i=0时,也就i指向散列表最首个元素索引的时候重新将mask赋值给i,使其指向散列表最后一个元素,重新开始反向遍历散列表,
    //其实就相当于绕圈,把散列表头尾连起来,不就是一个圈嘛,从begin值开始,递减索引值,当走过一圈之后,必然会重新回到begin值,
    //如果此时还没有找到key对应的bucket_t,或者是空的bucket_t,则循环结束,说明查找失败,调用bad_cache方法。
 
    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}


4.2 当 newOccupied <= capacity / 4 * 3

这是什么意思呢,就是第一个方法执行完之后,Occupied = 1 ,mask = 3,capacity = 4**
**进入第二个方法  newOccupied = Occupied + 1 为2   2小于缓存池整个空间的3/4,等于说整个缓存空间还可以容纳第二个方法,所以就在buckets(缓存池中)找到最合适的bucket(缓存桶)和上面通过cache 哈希寻找是一样的,将方法实现imp和key 关联起来.

4.3 当 newOccupied > capacity / 4 * 3

在这种情况下就需要扩容,扩大整个缓存池、扩大的空间为之前的两倍.

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}



void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();

    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

空间扩展完之后 设置新的缓存池,并且会把之前的缓存给清除,最后在buckets(缓存池中)找到最合适的bucket(缓存桶)和上面通过cache 哈希寻找是一样的,将方法实现imp和key 关联起来.

五、总结

  •  OC 中实例方法缓存在类上面,类方法缓存在元类上面。
  • cache_t 缓存会提前进行扩容防止溢出。
  • 方法缓存是为了最大化的提高程序的执行效率。
  • 苹果在方法缓存这里用的是开放寻址法来解决哈希冲突。

空间扩展完之后 设置新的缓存池,并且会把之前的缓存给清除,最后在buckets(缓存池中)找到最合适的bucket(缓存桶)和上面通过cache 哈希寻找是一样的,将方法实现imp和key 关联起来.

六、缓存流程图

image.png