objc_class中的cache_t

2020-09-21  本文已影响0人  CrazySnow

目标

主要分析cache_t流程,对象的属性、方法都会被iOS的缓存机制缓存下来,下次调用会从缓存中查找,缓存的功能流程主要是从一下三个环节分析:

一、cache_t的整体执行流程

cache_t功能流程.png

cache_t中的字段:
_buckets:数组,是bucket_t结构体的数组,bucket_t是用来存放方法的SEL内存地址IMP的;
_mask的大小是数组大小 - 1,用作掩码。(因为这里维护的数组大小都是2的整数次幂,所以_mask的二进制位000011, 000111, 001111)刚好可以用作hash取余数的掩码。刚好保证相与后不超过缓存大小。
_occupied是当前已缓存的方法数。即数组中已使用了多少位置。
_maskAndBucketmaskbucket的结合体,提升了性能和效率;

源码

struct cache_t {
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
public: //对外公开可以调用的方法
    static bucket_t *emptyBuckets();
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
   ......
}

struct bucket_t {
explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
public:
    inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); }

    inline IMP imp(Class cls) const {
        uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
        if (!imp) return nil;
}

cache属性的获取,需要通过pclass的首地址平移16字节,即首地址+0x10获取cache的地址

LLDB调试流程.png

cache的缓存原理及内存计算方式

缓存过程主要步骤:
1 、计算当前bucket占用量;
2 、根据bucketbuckets中的占用比,开辟空间
3、根据cache_hash方法,计算sel-imp存储的哈希下标,存入sel, imp, cls

cache_t::insert流程.png

ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif
    ASSERT(sel != 0 && cls->isInitialized());
/*
 step1 创建临时变量 newOccupied ,oldCapacity 
*/

    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;

/*
 step2 进行buckets的计算
*/
    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 <= capacity / 4 * 3)) { // 4  3 + 1 bucket cache_t
       //如果小于等于占用内存的3 /4 则什么都不用做。
    }
    else {
//扩容原因:第一次申请开辟的内存容量是 4 ,如果已经有3个bucket插入到cache里面,再次插入一个就会存满这个容量,为了保证读取的正确性,就对其进行扩容
// 扩容算法:有capacity时扩容为两倍,没有就初始化为INIT_CACHE_SIZE 也就是4
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true); 
 // 内存 库容完毕
    }
/*
 step3 计算好容量之后,进行插入sel imp class
*/

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m); //求cache的hash ,通过计算得到sel存储的下标
    mask_t i = begin;
//遍历操作 
    do {
// 如果sel 不存在就将sel存进去
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied(); //Occupied ++ 
            b[i].set<Atomic, Encoded>(sel, imp, cls); //存入  sel imp cls 
            return;
        }
// 如果sel 存在就返回
        if (b[i].sel() == sel) {
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}

补充

cache 缓存
bucket 桶,一桶的量
mask 面具;subnet mask ,子网掩码
occupied 已占用的;使用中的
capacity 容量; 能力
shift 转移, 移位
reallocate 重新分配
increment 增量

内存计算主要是根据cache_hash方法,即哈希算法,计算sel-imp存储的哈希下标,分为以下三种情况:

如果哈希下标的位置未存储sel,即该下标位置获取sel等于0,此时将sel-imp存储进去,并将occupied占用大小加1

如果当前哈希下标存储的sel 等于 即将插入的sel,则直接返回

如果当前哈希下标存储的sel不等于 即将插入的sel,则重新经过cache_next方法 即哈希冲突算法,重新进行哈希计算,得到新的下标,再去对比进行存储

cache_hash:哈希算法

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask; // 通过sel & mask(mask = cap -1)
}

cache_next:哈希冲突算法

#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask; //(将当前的哈希下标 +1) & mask,重新进行哈希计算,得到一个新的下标
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask; //如果i是空,则为mask,mask = cap -1,如果不为空,则 i-1,向前插入sel-imp
}

总结

什么时候触发cache缓存机制
objc_msgSend第一次发送消息会触发方法查找,找到方法后会调用cache_fill()方法把方法缓存到cache中,这个在后面分析方法的本质的时候会提到。

上一篇 下一篇

猜你喜欢

热点阅读