iOS开发cache_t(缓存)

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

struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic _buckets;
    explicit_atomic _mask;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic _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 _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 _imp;
    explicit_atomic _sel;
#else //非真机
    explicit_atomic _sel;
    explicit_atomic _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(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(缓存))