美文网首页ios底层原理
iOS看源码:cache_t方法缓存

iOS看源码:cache_t方法缓存

作者: FireStroy | 来源:发表于2020-09-18 08:19 被阅读0次

    上一篇文章已经写过了类的bits,现在来看看类的方法缓存cache 它是cache_t类型定义。在oc中,方法是以SEL-IMP这种成对的形式存放的。

    cache的内存结构
    //macos环境
    struct cache_t {
        explicit_atomic<struct bucket_t *> _buckets;
        explicit_atomic<mask_t> _mask;
    #if __LP64__
        uint16_t _flags;
    #endif
        uint16_t _occupied;
        struct bucket_t *buckets();
        //.…
    }
    
    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
        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;
            return (IMP)(imp ^ (uintptr_t)cls);
    #else
    }
    

    下面来通过调试来验证缓存cache的内存结构 断点在NSLog(@"");

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            MyClass *myClass = [[MyClass alloc]init];
            Class pclass = [MyClass class];
            [myClass say];
            NSLog(@"");   
        }
        return 0;
    }
    

    调试结果


    $0=0x00000001000022b8是MyClass类的isa地址 根据类的结构体内存结构 cache结构体的内存地址需要偏移16个字节(cache_t *) $1 = 0x00000001000022c8
    通过输出*$1可以获得cache_t的结构体内容(cache_t) $2如上图所示
    然后通过cache_t.bucket()可以获取到这个类存放方法缓存
    buckets数组的首地址,然后可以一次获取缓存过的方法SEL-IMP

    方法缓存的流程

    缓存cache的内存结构验证完了 接下来看看 方法是如何缓存的
    cache_t提供了一个void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)方法 这个方法就是用于方法缓存的插入
    最主要的流程如下:

    void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver){
    //...
    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 <= capacity / 4 * 3)) {
            // Cache is less than 3/4 full. Use it as-is.
        }
        else {
            capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
            if (capacity > MAX_CACHE_SIZE) {
                capacity = MAX_CACHE_SIZE;//最大值为16
            }
            reallocate(oldCapacity, capacity, true);
        }
        bucket_t *b = buckets();
        mask_t m = capacity - 1;
        mask_t begin = cache_hash(sel, m);
        mask_t i = begin;
        do {
            if (fastpath(b[i].sel() == 0)) {
                incrementOccupied();
                b[i].set<Atomic, Encoded>(sel, imp, cls);
                return;
            }
            if (b[i].sel() == sel) {
                return;
            }
        } while (fastpath((i = cache_next(i, m)) != begin));
    }
    

    每次有新方法缓存的时候occupied +1
    方法缓存的起始容量是INIT_CACHE_SIZE初始值是4
    每当缓存容量的使用>=3/4时 缓存就会扩容 在原来容量基础上翻倍
    容量最大为16

    缓存空间的开辟与初始赋值

    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) {
            cache_collect_free(oldBuckets, oldCapacity);
        }
    }
    
    void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
    {
        _buckets.store(newBuckets, memory_order::memory_order_release);
        _mask.store(newMask, memory_order::memory_order_release);
        _occupied = 0;
    }
    

    缓存的创建和扩容 都会用到reallocate()这个函数 内部会依据当前的capacity最终调用calloc()这个函数来分配一个内存空间并初始化
    每一次新扩容之后进行 _occupied = 0_mask = newMask - 1初始化赋值。
    每次扩容之后 旧的缓存被释放 新的缓存空间被创建并且初始化。
    不过根据文档的解释 旧的缓存空间不是立刻被释放 而是在稍后的某个点才会被释放。

    方法的缓存方式

    当有新的方法需要放进缓存空间的时候,新的缓存是如何加入到缓存空间的呢?

        mask_t m = capacity - 1;
        mask_t begin = cache_hash(sel, m);
        mask_t i = begin;
    //…
          if (fastpath(b[i].sel() == 0)) {
                incrementOccupied();
                b[i].set<Atomic, Encoded>(sel, imp, cls);
                return;
            }
    //...
    
    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask;
    }
    
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask;
    }
    

    新的selmask进行了一次哈希运算得到一个下标,并依据这个下标处存放的内容来进行存放。
    读取的时候也只需要sel通过哈希运算即可快速取得下标从而拿到函数的imp地址

    相关文章

      网友评论

        本文标题:iOS看源码:cache_t方法缓存

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