OC底层原理-objc 818(四)cache_t

前言

我们已经了解了objc_class结构体的内容了,也分析了isa和superclass的功能,我们在来看看objc_class结构体,来开启我们cache的探索之路。

struct objc_class : objc_object {
    // Class ISA;
    // Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;  
}
复制代码

找到cache的地址

首先我们可以通过p/x打印出对象的首地址,我们知道每个指针占8字节,那么isa和superclass都是指针各占8字节,那么我们只要内存平移16字节0x10,就可以找到cache的首地址了。

cache_t结构体源码

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;	// uintptr_t = long 占8字节
    union {												// 共用体,共用体的内存大等于成员中最大的成员占内存,所以占8字节
        struct {
            explicit_atomic<mask_t>    _maybeMask;		// mask_t = int32 占4字节
#if __LP64__
            uint16_t                   _flags;			// 占2字节
#endif
            uint16_t                   _occupied;		// 占2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;		// 指针占8字节
    };
    
    // 省略……
	
    // occupied++ 插入bucket时调用,缓存计数+1
    void incrementOccupied();
    // 初始化cache_t
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    // 获取buckets
    struct bucket_t *buckets() const;
    // 存储元素个数
    mask_t occupied() const;
    // 向缓存中插入bucket
    void insert(SEL sel, IMP imp, id receiver);
    
}
复制代码

_bucketsAndMaybeMask中存储这bucketsmask信息,是在setBucketsAndMask()方法中进行处理

buckets:类似数组,内存连续,存储着缓存的方法

mask:方法查找和插入时的掩码

occupied:记录缓存的方法数量,每次插入新方法时调用incrementOccupied()进行计数+1

insert方法分析

先来看看insert方法源码

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

    // 前边忽略,到此处说明需要添加新的缓存
    // 计算新的occupied
    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;

    // hash运算进行存储
    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
}
复制代码

初始化创建cache_t

if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
复制代码

初始化容量

初始化容量为4(1 << 2 )

INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2) = 1 << 2

INIT_CACHE_SIZE_LOG2 = 2

reallocate函数初始化

reallocate实现

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}
复制代码
  1. 首先获取旧的buckets;
  2. 再创建一个新的容量的buckets;
  3. 调用setBucketsAndMask函数将buckets和mask关联,mask的值就是容量值-1
  4. 判断是否需要释放旧的缓存,扩容的时候需要释放旧的缓存
  5. 释放旧的缓存

扩容与collect_free释放旧的缓存

扩容判断

通过fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)判断是否需要扩容

newOccupied:缓存数量;

CACHE_END_MARKER:1;

cache_fill_ratio(capacity):capacity * 3 / 4;

也就是说,当缓存的元素个数大与容量的3/4时,进行扩容。

扩容

capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;

可以看到,扩容时会将容量扩容到原容量的2倍

collect_free实现

void cache_t::collect_free(bucket_t *data, mask_t capacity)
{
    _garbage_make_room ();
    garbage_byte_size += cache_t::bytesForCapacity(capacity);
    garbage_refs[garbage_count++] = data;
    cache_t::collectNolock(false);
}
复制代码

扩容为什么要释放旧的缓存信息

由于缓存的存储方式采用的是hash表的方式存储,通过cache_hash(sel, m)计算hash表的key,mask的值时容量大小减1,那么扩容后容量变了,所以mask的值也变了,所以需要清空所有缓存重新进行方法缓存。

Hash存储Key计算

// hash运算进行存储
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));	// 掩码运算索引
复制代码
  1. 获取buckets

bucket_t *b = buckets();

  1. 通过缓存容量计算出mask

mask_t m = capacity - 1;

  1. 通过sel和mask获取方法的hash值

mask_t begin = cache_hash(sel, m);

mask_t i = begin;

  1. 通过hash值和mask获取key

i = cache_next(i, m)

cache_next内部实质就是将hash值+1后再重新进行掩码运算,直到满足条件后存储缓存,循环结束

  1. fastpath(b[i].sel() == 0

判断buckets中对应的key值是否为空,如果为空的话将该方法插入到对应位置,结束循环,否则hash值加1继续掩码运算

代码查看

先找到最初始的cache信息

调用test方法后再查看缓存信息

查看缓存的方法信息,通过bucket_t调用sel查看缓存的方法信息

inline SEL sel() const { return_sel.load(memory_order_relaxed); }

通过bucket_t调用imp查看函数指针

inline IMP imp (UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)

猜你喜欢

转载自juejin.im/post/7016957331753664548