十、浅谈cache_t

48 阅读4分钟

本文由快学吧个人写作,以任何形式转载请表明原文出处

一、资料准备

objc4-818.2 : github.com/LGCooci/KCO…

对应mac的版本是11.1。可根据自己的系统版本挑选可以进行调试的源码。

二、cache的结构

  1. 进源码找到类的结构那里

图片.png

  1. 类型是cache_t,所以进cache_t的源码 :

(1). 发现cache_t是一个结构体,有私有和公开两部分代码。

(2). 有很多的宏定义判断。

判断很多,说明对应不同的环境会用不同的代码,不具备统一普遍性,那么先看没有宏定义判断的,整理一些没有宏定义,具有普遍性的源码出来。


struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //8字节
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
    ...
    ...
    bool isConstantEmptyCache() const;
    bool canBeFreed() const;
    mask_t mask() const;
    
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);

    static bucket_t *emptyBuckets();
    static bucket_t *allocateBuckets(mask_t newCapacity);
    static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));  
public:
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;
    mask_t occupied() const;
    
    void initializeToEmpty();
    void insert(SEL sel, IMP imp, id receiver);
    void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
    void destroy();
    void eraseNolock(const char *func);
    
    static void init();
    static void collectNolock(bool collectALot);
    static size_t bytesForCapacity(uint32_t cap);
  1. 简单的说,cache具备普遍性的源码就如上面所示。

三、cache缓存的是什么

从上面的源码中可以看到,cache有一个insert()函数。也就是插入函数,插入的内容应该就是存储的内容。

cahche_t的insert插入了 : SEL,IMP,和一个接收者。由此可以判断,cache缓存的是类的方法。

四、cache是如何缓存类的方法的

进入insert()函数的实现,整理出对我们来说更必要的代码

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    //缓存写入的时候要加锁,防止出现同时操作同一对象。
    runtimeLock.assertLocked();

    //在类完成初始化之前,是绝不会进行缓存的
    if (slowpath(!cls()->isInitialized())) {
        return;
    }
    
    //常量优化缓存不会进行
    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }
...
...
    // 按照原样进行缓存,直到超过了预期的填充率
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // 缓存是只读的,替换掉
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // 缓存不足3/4或者7/8,按照原样使用
    }
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    // 搜索第一个未使用的插槽,并插入到这个插槽。
    // 保证会有一个空位
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

1. 谁在存储类的方法?

  1. 从上述源码可以看出在do...while循环中,变量b是进行了set操作的,也就是说,缓存实际是在这里进行的。对于变量b :

(1). 是bucket_t类型的指针。

(2).并且是通过一个buckets()函数获取的

  1. bucket_t是什么,下面是整理后的源码,因为源码也很多宏判断
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
public:
    static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    
    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {...}
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
}
  1. bucket_t源码可以发现,bucket_t存储的就是类的IMP和SEL,并且在注释中得到信息 :

(1).对于arm64e和arm64(也就是真机调试),bucket_t存储类的方法的方式是IMP在前

(2).对于x86,i386,armv7(模拟器和其他设备),bucke_t存储类的方法的方式是SEL在前

(3). sel()函数获取方法的名称

(4). imp()函数获取方法的实现

结论 : bucket_t在存储类的方法。

2. 如何缓存的类的方法

看完bucket_t的源码,知道了insert()中的b变量就是用来存储类的方法和名称的变量。那么如何存储的就要看do...while

图片.png

3. 有关缓存的容量

缓存既然要存储类的方法和imp,那么肯定是要有内存的,那么缓存的容量是怎么来的?源码也是在insert()函数中,在do...while的上面。

图片.png

  1. occupied是指占用的空间。capacity是指容量。

  2. 第一个if就是说如果cache还没有使用,没有空间的时候,需要初始化缓存的大小。

先给容量赋值 : capacity = INIT_CACHE_SIZE;

然后申请内存空间 : reallocate(),进入这个函数 :

图片.png

  1. 第二个if中已经写了备注了缓存的空间占用没有到全部空间的3/4或者7/8,则可以继续使用。