美文网首页
objc_class中的cache_t

objc_class中的cache_t

作者: CrazySnow | 来源:发表于2020-09-21 10:55 被阅读0次

    目标

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

    • macOS i386
    • 模拟器 : x86
    • 真机 : arm64

    一、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
    • 从源码的分析中,我们知道sel-imp是在cache_t_buckets属性中(目前处于macOS环境),而在cache_t结构体中提供了获取_buckets属性的方法buckets();
    • 获取了_buckets属性,就可以获取sel-imp了,这两个的获取在bucket_t结构体中同样提供了相应的获取方法sel()以及 imp(pClass).

    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中,这个在后面分析方法的本质的时候会提到。

    相关文章

      网友评论

          本文标题:objc_class中的cache_t

          本文链接:https://www.haomeiwen.com/subject/uzgmyktx.html