美文网首页iOS底层原理
iOS开发cache_t(缓存)

iOS开发cache_t(缓存)

作者: 爱看书de图图 | 来源:发表于2020-09-19 15:43 被阅读0次

      在上一篇文章里iOS开发之类的本质里,我们详细研究了bits,我们用内存偏移得出的,我们计算了cache_t的大小,然后用lldb打印出了bits里面的内容。今天,我们来研究,我们跳过的,cache_t里面究竟存放了什么东西。我们先来进去看看cache_t的结构:

    struct cache_t {
        #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets;
        explicit_atomic<mask_t> _mask;
        #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
        static constexpr uintptr_t maskShift = 48;
        static constexpr uintptr_t maskZeroBits = 4;
        static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
        static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
        static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS,
        "Bucket field doesn't have enough bits for arbitrary pointers.");
        #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
        #if __LP64__
        uint16_t _flags;
        #endif
        uint16_t _occupied;
    }
    

    这里有不同的架构处理方式:

    • CACHE_MASK_STORAGE_OUTLINED表示macOSi386的架构。
    • CACHE_MASK_STORAGE_HIGH_16表示真机,arm64的架构。
    • CACHE_MASK_STORAGE_LOW_4表示模拟器,x86的架构。
      我们继续查看里面的bucket_t源码,里面有两个版本,真机和非真机,只是selimp的顺序不同
    struct bucket_t {
    private:
    #if __arm64__ //真机
        //explicit_atomic 是加了原子性的保护
        explicit_atomic<uintptr_t> _imp;
        explicit_atomic<SEL> _sel;
    #else //非真机
        explicit_atomic<SEL> _sel;
        explicit_atomic<uintptr_t> _imp;
    #endif
        //方法等其他部分省略
    }
    

    现在我们查找cache中的sel和imp,我们先定义一些方法和属性,并实现。

    @interface Person : NSObject{
        NSString *name;
    }
    @property (nonatomic, copy)NSString *nickname;
    @property (nonatomic, copy)NSString *height;
    -(void)eat;
    -(void)drink;
    -(void)say;
    +(void)run;
    @end
    

    然后我们调用

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...  class_data_bits_t
            Person *person = [Person alloc];
            
            [person eat];
            [person say];
            [person drink];
            [Person run];
            NSLog(@"Hello, World!");
        }
        return 0;
    }
    

    添加断点


    接下来又是我们熟悉的lldb调试:

    • p $3.buckets()是因为我们在cache_t的源码里,提供了这个方法:

      同样的,p $5.sel()/p $5.imp(p)也是cache_t的源码里提供的方法:

    通过我们上面的调试,我们看到,第一次断点的时候,cache_t的缓存中,_occupied = 0,在第二次断点的时候,我们看到了,里面有了方法的缓存,然后我们打印出了eat()方法。我们再执行一步方法,然后看看缓存中是否多了内容。


    可以看到,在执行了say()方法后,我们成功的缓存中打印出了say()方法。这里p($9+1)就用到了我们之前说到的指针偏移。你也可以p $8.buckets()[0]/p $8.buckets()[1]
    脱离源码环境通过项目查找

      我们之前的调试都是通过源码环境进行的,那么我们能不能脱离源码环境来进行查找呢?我们可以模拟源码环境。然后将需要的源码进行设计,拷贝到项目中,模拟一下方法的写入流程:

    typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    
    struct hk_bucket_t {
        SEL _sel;
        IMP _imp;
    };
    
    struct hk_cache_t {
        struct hk_bucket_t * _buckets;
        mask_t _mask;
        uint16_t _flags;
        uint16_t _occupied;
    };
    
    struct hk_class_data_bits_t {
        uintptr_t bits;
    };
    
    struct lg_objc_class {
        Class ISA;
        Class superclass;
        struct hk_cache_t cache;             // formerly cache pointer and vtable
        struct hk_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    };
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...  class_data_bits_t _buckets
            Person *person = [Person alloc];
            Class p = [Person class];
            
            [person say1];
            [person say2];
    //        [person say3];
    //        [person say4];
    //        
            struct lg_objc_class *hk_pClass = (__bridge struct lg_objc_class *)(p);
            NSLog(@"%hu - %u",hk_pClass->cache._occupied,hk_pClass->cache._mask);
            for (mask_t i = 0; i < hk_pClass->cache._mask; i++) {
                // 打印获取的 bucket
                struct hk_bucket_t bucket = hk_pClass->cache._buckets[i];
                NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
            }
            
            NSLog(@"Hello, World!%@",p);
        }
        return 0;
    }
    

    看一下输出结果:


    然后我们打开注释的say3()say4()方法,看看打印结果:
    发现有变化的是_occupied_mask,从2-3变成了2-7,那么他们分别是什么意思呢?并且say3(),say4()的调用顺序貌似有问题,和我们调用的顺序不太一样。我们看cache_t源码里的方法:

    然后我们全局搜索这个方法,我们找到了cache_t的插入方法:
    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;
            }
            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) {
                // 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));
    }
    
    • 如果有内容插入就会调用代码:mask_t newOccupied = occupied() + 1;
    • 然后进入判断,capacity = INIT_CACHE_SIZEINIT_CACHE_SIZE在这里等于4,然后开辟空间函数reallocate()
      然后我们看一下这个函数:
    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
    {
        bucket_t *oldBuckets = buckets();
        bucket_t *newBuckets = allocateBuckets(newCapacity);
        ASSERT(newCapacity > 0);
        ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    
        setBucketsAndMask(newBuckets, newCapacity - 1);
        
        if (freeOld) {
            cache_collect_free(oldBuckets, oldCapacity);
        }
    }
    
    • 里面有个写入的函数allocateBuckets(),再往里面就是我们熟悉的calloc函数,然后还有个函数setBucketsAndMask(),这个函数的作用是为了写入cache_t
    • 我们继续流程分析,往下继续判断,// Cache is less than 3/4 full. Use it as-is.就是如果大于3/4,就会进行扩容,我们一开始申请了4个,然后到第3个的时候,我们就会*2,所以我们继续往下看到了mask_t m = capacity - 1;,这就是为什么我们打印出来的mask是3和7了,因为3 = 4-17 = 4*2-1
    • 在扩容算法里,我们有个reallocate(oldCapacity, capacity, true);函数,跟上面的一样,里面有个判断,freeold,然后调用了cache_collect_free(oldBuckets, oldCapacity);,我们继续看一下具体实现:
    static void cache_collect_free(bucket_t *data, mask_t capacity)
    {
    #if CONFIG_USE_CACHE_LOCK
        cacheUpdateLock.assertLocked();
    #else
        runtimeLock.assertLocked();
    #endif
        if (PrintCaches) recordDeadCache(capacity);
        _garbage_make_room ();
        garbage_byte_size += cache_t::bytesForCapacity(capacity);
        garbage_refs[garbage_count++] = data;
        cache_collect(false);
    }
    

    里面有个_garbage_make_room ();函数,继续往里看:

    static void _garbage_make_room(void)
    {
        static int first = 1;
    
        // Create the collection table the first time it is needed
        if (first)
        {
            first = 0;
            garbage_refs = (bucket_t**)
                malloc(INIT_GARBAGE_COUNT * sizeof(void *));
            garbage_max = INIT_GARBAGE_COUNT;
        }
    
        // Double the table if it is full
        else if (garbage_count == garbage_max)
        {
            garbage_refs = (bucket_t**)
                realloc(garbage_refs, garbage_max * 2 * sizeof(void *));
            garbage_max *= 2;
        }
    }
    

    这里才找到了真的扩容函数,所做的事情,就是重新申请内存空间realloc(garbage_refs, garbage_max * 2 * sizeof(void *));``garbage_max = INIT_GARBAGE_COUNT;``INIT_GARBAGE_COUNT = 128

    总结:
    1、_mask是什么?

    _mask是指掩码数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask等于capacity - 1

    2、_occupied 是什么?

    _occupied表示哈希表中sel-imp的占用大小 (即可以理解为分配的内存中已经存储了sel-imp的的个数),所有的方法调用都会影响_occupied,包括init方法。

    4、bucket数据为什么会有丢失的情况?,例如2-7中,只有say3、say4方法有函数指针

    原因是在扩容时,是将原有的内存全部清除了,再重新申请了内存导致的

    5、2-7中say3、say4的打印顺序为什么是say4先打印,say3后打印,且还是挨着的,即顺序有问题?

    因为sel-imp的存储是通过哈希算法计算下标的,其计算的下标有可能已经存储了sel,所以又需要通过哈希冲突算法重新计算哈希下标,所以导致下标是随机的,并不是固定的

    相关文章

      网友评论

        本文标题:iOS开发cache_t(缓存)

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