OC类结构之cache分析

191 阅读5分钟

cache的结构

通过OC对象本质及isa结构分析我们已经知道了类的结构体中有一个cache_t类型的cache

下面我们看一下cache_t的结构:

首先介绍一下这三个宏

  • CACHE_MASK_STORAGE_OUTLINED表示cache的结构是在x86和i386,即macOS和模拟器下的;
  • CACHE_MASK_STORAGE_HIGH_16表示在64位系统下的cache结构;
  • CACHE_MASK_STORAGE_LOW_4表示在32位系统下的cache结构。

  • 模拟器下有_buckets_mask两个元素;
  • 真机下有_maskAndBuckets_mask_unused

下面还有两个元素:

  • 64位系统下有_flags;
  • _occupied是通用的元素。

下面我们通过lldb调试来验证cache的结构

我们在mac上运行OC语言781的源码环境,创建LGPerson类,初始化一个对象,并断点:

lldb调试过程如下:

这样我们就成功的打印出了cache在类中的结构,证明了cache_t结构体在模拟器架构下是由_buckets,_mask,_flags,_occupied组成的,同理可以证明真机下的cache_t结构体组成。

有关lldb调试和指针偏移请参考OC对象本质及isa结构分析

cache存的是什么?

首先来看一下_buckets的结构

可以看到_buckets只有selimp两个元素

  • 通过sel()可以读取到sel
  • 通过imp(Class cls)可以读取到imp

cache_t中有一个buckets()函数可以获取到_buckets的内容

所以我们可以通过lldb打印出buckets()的内容

由此说明 cache中缓存的其实就是类的方法名和方法实现

cache的存储逻辑

通过上面的分析和OC对象本质及isa结构分析我们已经类的结构体、cache_tbucket_t的结构体的组成,并且通过OC对象本质及isa结构分析知道了OC类的本质是结构体,以及isa的指向。有了这些认知,我们就可以自己构造一个环境,打印出cache_t的结构中buckets_occupied_mask的具体内容

构造环境

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct lg_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct lg_cache_t {
    struct lg_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct lg_class_data_bits_t {
    uintptr_t bits;
};

struct lg_objc_class {
    Class ISA;
    Class superclass;
    struct lg_cache_t cache;             // formerly cache pointer and vtable
    struct lg_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

将类构造成结构体,并打印类的cache中的buckets_occupied_mask的具体内容

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];  // objc_clas
        [p say1];
        [p say2];
//        [p say3];
//        [p say4];
         
        struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
        NSLog(@"_occupied = %hu - _mask = %u",lg_pClass->cache._occupied,lg_pClass->cache._mask);
        for (mask_t i = 0; i<lg_pClass->cache._mask; i++) {
            // 打印获取的 bucket
            struct lg_bucket_t bucket = lg_pClass->cache._buckets[i];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }

        
        NSLog(@"Hello, World!");
    }
    return 0;
}

当只调用了say1say2的对象方法时的打印如下:

当打印了say1say2say3say4的对象方法时的打印如下:

通过两次打印的对比,有以下几个疑问:

  • 我们前后调用了二个方法和四个方法,为什么buckets会被打印3次和7次,并且第二次的打印是无序的?
  • 为什么_occupied两次打印的结果一样?
  • 为什么_mask一次是3,一次是``?

带着这些疑问,我们继续探索cachet_t的结构中调用的一些函数

通过查看cache_t的原码我们发现在结构体的最后面有一个insert函数

这个就是cache的写入函数,我们来看insert的实现

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());
    // Use the cache as-is if it is less than 3/4 full
    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)) { // 4  3 + 1 bucket cache_t
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍 4
        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;
    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the
    // minimum size is 4 and we resized at 3/4 full.
    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));
    cache_t::bad_cache(receiver, (SEL)sel, cls);
}

我们分段分析

if (slowpath(isConstantEmptyCache()))

  • unsigned oldCapacity = capacity(), capacity = oldCapacity;获取当前cache缓存空间的大小;
  • if (!capacity) capacity = INIT_CACHE_SIZE;限制初始化的最大空间大小为INIT_CACHE_SIZE = INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2) = 4;
  • reallocate(oldCapacity, capacity, /* freeOld */false);是初始化buckets存储空间

else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3))

  • mask_t newOccupied = occupied() + 1; 拿到当前cache缓存空间的大小,并+1;
  • newOccupied + CACHE_END_MARKER <= capacity / 4 * 3判断是否需要扩容,条件是小于当前空间大小的四分之三;

else

  • capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;扩容原来的二倍大小
  • if (capacity > MAX_CACHE_SIZE) { capacity = MAX_CACHE_SIZE; }最大容量为MAX_CACHE_SIZE = MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2)=2的15次方;
  • reallocate(oldCapacity, capacity, true);扩容操作,此时会直接干掉之前的存储空间,开辟新的存储空间。这样做虽然会导致之前的缓存被清空,但是重新开辟的成本小,速度快。

  • mask_t m = capacity - 1;获取当最可以存储数据的最大值;
  • 通过hash算法,得到要插入的位置,这样得到的插入位置是无序的没有规律的

do While循环找到可以插入的位置

  • if (fastpath(b[i].sel() == 0)) 说明这个位置没有存数据,可以存了;
  • if (b[i].sel() == sel)这个位置存了数据了,执行下一次循环;

  • 数据存好后_occupied++

  • 通过再hash找到下一个可以存储数据的位置

通过上面的分析可以得出结论:

  • _mask是当前存储空间大小-1;
  • _occupied是当前实际存储的数据量;
  • cache的初始空间大小是4,后面以2倍扩容,最大为2的15次方
  • 插入的位置是以selmask进行hash得到的,所以存储的数据是无序的

于是上面的几个疑问也就解决了~~~

下面提供一张cache结构及流和分析图