objc_class底层cache_t详解

2022-05-04  本文已影响0人  linc_

cache_t 结构解析

类的底层原理探索 中我们了解了objc_class中存储了isa,superClass,cache,bits,今天我们来看下cache的作用和底层实现。

cache结构

image.png
这个结构并不能看出来cache的作用,所以我们通过内存偏移打印一下cache的内容
image.png

我们打印的信息和其数据结构一致,但是缓存的内容我们还是不知道,我们只能从源码中继续往下看。

image.png

原码中有一个insert方法,应该是用来存入数据的。

进入这个insert函数来看一下


image.png

我们注意到insert方法的参数有SEL,IMP,receiver,并且将这些参数放在了bucket中,我们来验证一下


获取bucket_t的内存内容

bucket_t看到了sel和imp,但是输出的内容又看不懂了,来看看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.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

    // Compute the ptrauth signing modifier from &_imp, newSel, and cls.
    uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
        return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }

    // Sign newImp, with &_imp, newSel, and cls as modifiers.
    uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        return (uintptr_t)
            ptrauth_auth_and_resign(newImp,
                                    ptrauth_key_function_pointer, 0,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

public:
    static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }

#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
    inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
        return (IMP)imp;
    }

    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

    inline void scribbleIMP(uintptr_t value) {
        _imp.store(value, memory_order_relaxed);
    }

    template <Atomicity, IMPEncoding>
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

我们看到 调用sel()函数可以返回SEL,调用imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)可以返回IMP,于是就可以试试在内存上获取到的bucket_t去调用这两个函数:


调用sel()和imp()函数 cache_t

cache_t扩容

那方法能存多少呢?存满了又是如何扩容呢?buckets是如何扩容的?为什么我没调用class和respondsToSelector方法它们就缓存到了buckets里面了?
看下cache_t的insert函数的实现代码

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; // 第一次insert的时候occupied()即_occupied会是0,newOccupied会是1
    // capacity的值就是buckets的长度
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    // 如果cache为空,则分配 arm64下长度为2 x86_64下长度为4的buckets,reallocate里无需释放老buckets
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        // 给容量附上初始值,x86_64为4,arm64为2
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    // 在arm64下,缓存的大小 <= buckets长度的7/8  不扩容
    // 在x86_64下,缓存的大小 <= buckets长度的3/4  不扩容
    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 // 只有arm64才需要走这个判断
    // 在arm64下,buckets的长度 < = 8 时,不扩容
    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 { // 除却上面的逻辑,就是扩容逻辑了
        // 对当前容量的2倍扩容,并且如果扩容后容量大小 大于 一个最大阈值,则设置为这个最大值
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        // 创建新的扩容后的buckets,释放旧的bukets
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets(); // 获取buckets数组指针
    mask_t m = capacity - 1; // m是buckets的长度-1
    mask_t begin = cache_hash(sel, m);// 通过hash计算出要插入的方法在buckets上的起始位置(begin不会超过buckets的长度-1)
    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)) { // 当前hash计算出来的buckets在i的位置它有没有值,如果没有值就去存方法
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) { // 当前hash计算出来的buckets在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)); // 如果计算出来的起始位置i存在hash冲突的话,就通过cache_next去改变i的值(增大i)

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

从代码上看,第一次空了会进行初始化capacity的长度INIT_CACHE_SIZE.


image.png
image.png

在arm64架构下开辟一个长度为2的桶子,在x86_64架构下开辟长度为4的桶子

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.
    }

在arm64架构下如果缓存大于等于桶子长度的7/8,在x86_64架构下缓存大小大于等于桶子长度的3/4 则什么也不干。
在arm64架构下,当桶子的长度小于等于8的时候什么也不干
当缓存长度大于(系统设定的)默认最大值就等于默认最大值
其他情况下需要扩容,扩容的大小为2倍。

所以我们就明白了为什么之前调用了方法之后会什么没有找到,arm64下,初始值为2,当第1个方法缓存的时候,则要进行两倍扩容为4,并且需要清除旧桶。所以instanceMethod在刚进来扩容的时候就被清除掉了,也就找不到了。而前面我们说到class方法和responseToSelector方法是我们在调试的时候通过lldb调用产生的。

上一篇下一篇

猜你喜欢

热点阅读