iOS底层原理05-cache详解

429 阅读3分钟

这篇,我们来探索objc_class中的cache_t,字面意思很好理解,cache就是缓存,具体cache原理是什么呢?又缓存了什么东西?我们来一探究竟。

cache_t结构

image.png

image.png

我们看到了void insert(SEL sel, IMP imp, id receiver); 这个insert方法里面有selimpreceiver这些参数。看出来这事一个添加方法到cache_t的函数。我们进到里面看看具体做了什么。

void cache_t::insert(SEL sel, IMP imp, id receiver)

{

    runtimeLock.assertLocked();


    // Never cache before +initialize is done

    if (slowpath(!cls()->isInitialized())) {

        return;

    }


    if (isConstantOptimizedCache()) {

        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",

                    cls()->nameForLogging());

    }


#if DEBUG_TASK_THREADS

    return _collecting_in_critical();

#else

#if CONFIG_USE_CACHE_LOCK

    mutex_locker_t lock(cacheUpdateLock);

#endif


    ASSERT(sel != 0 && cls()->isInitialized());


    // Use the cache as-is if until we exceed our expected fill ratio.

    mask_t newOccupied = occupied() + 1;

    unsigned oldCapacity = capacity(), capacity = oldCapacity;

    if (slowpath(isConstantEmptyCache())) {

        // Cache is read-only. Replace it.

        if (!capacity) capacity = INIT_CACHE_SIZE;

        reallocate(oldCapacity, capacity, /* freeOld */false);

    }

    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {

        // Cache is less than 3/4 or 7/8 full. Use it as-is.

    }

#if CACHE_ALLOW_FULL_UTILIZATION

    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {

        // Allow 100% cache utilization for small buckets. Use it as-is.

    }

#endif

    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;

    // Scan for the first unused slot and insert there.

    // There is guaranteed to be an empty slot.

    do {

        if (fastpath(b[i].sel() == 0)) {

            incrementOccupied();

            b[i].set<Atomic, Encoded>(b, sel, imp, cls());

            return;

        }

        if (b[i].sel() == sel) {

            // The entry was added to the cache by some other thread

            // before we grabbed the cacheUpdateLock.

            return;

        }

    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);

#endif // !DEBUG_TASK_THREADS

}

这段代码里,我们发现b.set<Atomic, Encoded>(b, sel, imp, cls()); 方法把sel,imp,cls等添加到了b=bucket_t的结构体里,还有buckets()capacitycache_hash(sel, m)他们又做了什么? 让我们来逐一分析他们。

验证bucket_t 用来存储sel, imp

image.png

但是当我们多调用一个method1时,奇怪的事情却发生了...

原本存在buckets里面的方法都没有了

cache扩容规则

image.png

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

真机验证

我们的对象有n个静态方法,我们依次运行看看cache情况。

image.png

image.png

疑问解答

在阅读源码的时候,可能会有一些疑惑,这里我来归纳一下

1、bucket数据为什么会有丢失的情况

原因是在扩容时,是将原有的内存全部清除了,再重新申请内存导致的。

2、_mask是什么?

_mask是指掩码数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask 等于capacity - 1。

3、_occupied 是什么?

_occupied表示哈希表中 sel-imp 的占用大小 (即可以理解为分配的内存中已经存储了sel-imp的的个数)。

  • init会导致occupied变化

  • 属性赋值,也会隐式调用,导致occupied变化

  • 方法调用,导致occupied变化

4、打印的 cache_t 中的 ocupied 为什么是从 2 开始?

这里是因为LGPerson通过alloc创建的对象,并对其两个属性赋值的原因,属性赋值,会隐式调用set方法,set方法的调用也会导致occupied变化。