类结构探究(三)-- cache分析

之前我们已经探究了bits的结构,本文将对类的一个重要成员--cache,从源码objc4-7.8.1层面进行分析。

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;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    explicit_atomic _maskAndBuckets;
    mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
};

可以看到,cache_t主要有4个成员--buckets、mask、flags、occupied。其中模拟器将buckets和mask分开,而在真机中则将mask和buckets合并为一个变量_maskAndBuckets。

那么,这些成员分别保存了什么信息呢?接下来一一探讨。

模拟源码分析

explicit_atomic实际上只是起到一个线程安全的作用,_buckets实际上是bucket_t *类型。

查看bucket_t的结构,发现它有_imp和_sel变量,我们可以猜测,它是存放缓存方法信息的变量。

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 _imp;
    explicit_atomic _sel;
#else
    explicit_atomic _sel;
    explicit_atomic _imp;
#endif

    ...
};

我们可以通过一段代码进行验证。

同样是创建Animal类,添加4个方法

@interface Animal : NSObject

- (void)func1;

- (void)func2;

- (void)func3;

- (void)func4;

@end


@implementation Animal

- (void)func1 {
    NSLog(@"%s", __func__);
}

- (void)func2 {
    NSLog(@"%s", __func__);
}

- (void)func3 {
    NSLog(@"%s", __func__);
}

- (void)func4 {
    NSLog(@"%s", __func__);
}

@end

然后我们模拟一个objc_class类,将Animal的类转化为相同我们类结构,相当于一个镜像,从而可以在非源码环境获取类结构体中的信息。

代码如下:

typedef uint32_t mask_t;

struct lj_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct lj_cache_t {
    struct lj_bucket_t * _buckets;
    mask_t _mask;
    
    uint16_t _flags;
    uint16_t _occupied;
};

struct lj_class_data_bits_t {
    uintptr_t bits;
};

struct lj_objc_class {
    Class ISA;
    Class superclass;
    struct lj_cache_t cache;
    struct lj_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Animal *obj = [Animal alloc];
        Class pClass = [Animal class];
        
        struct lj_objc_class *lj_pClass = (__bridge struct lj_objc_class *)(pClass);
        NSLog(@"before occupied: %hu, mask: %u", lj_pClass->cache._occupied, lj_pClass->cache._mask);
        
        [obj func1];
        [obj func2];
        
        NSLog(@"after occupied: %hu, mask: %u", lj_pClass->cache._occupied, lj_pClass->cache._mask);
        
        for (mask_t i = 0; icache._mask; i++) {
            struct lj_bucket_t bucket = lj_pClass->cache._buckets[i];
            NSLog(@"%@ - %p", NSStringFromSelector(bucket._sel), bucket._imp);
        }
    }
    return 0;
}

执行结果为:

2020-09-17 18:24:22.145891+0800 cache_t分析[28651:4825405] before occupied: 0, mask: 0
2020-09-17 18:24:22.145979+0800 cache_t分析[28651:4825405] -[Animal func1]
2020-09-17 18:24:22.146181+0800 cache_t分析[28651:4825405] -[Animal func2]
2020-09-17 18:24:22.146241+0800 cache_t分析[28651:4825405] after occupied: 2, mask: 3
2020-09-17 18:24:22.146827+0800 cache_t分析[28651:4825405] func2 - 0x2ca8
2020-09-17 18:24:22.146903+0800 cache_t分析[28651:4825405] (null) - 0x0
2020-09-17 18:24:22.147001+0800 cache_t分析[28651:4825405] func1 - 0x2cd8

我们可以看到,fun1和fun2执行之前,mask和occupied都为0,而在执行fun1和fun2之后,mask为3,occupied为2,同时在buckets中存储了fun1和fun2,并且不是顺序排列。

目前我们可以确定的是,方法缓存保存在buckets中。但是此时我们有了两个新问题:

  1. mask和occupied分别表示什么
  2. 为什么buckets的存放时乱序的

回归源码探究

重新回到objc4-7.8.1源码,我们可以看到在objc-runtime-new.h第317行有一个occupied的自增方法incrementOccupied(),我们可以猜测答案与这个方法有关。全局搜索incrementOccupied,可以发现objc-cache.mm中的cache_t::insert中调用了这个方法,则显然是一个插入方法缓存的方法,答案就这这个方法中。

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());

    // 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)) {
        // 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;

    // 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(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);
}

这部分代码较多,分析之后可以大体分为2部分

1.开辟空间

开辟空间有3个分支,我们一一来看。

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);
}

1.1 isConstantEmptyCache()--当前缓存是否为空

当前对象如果没有调用过任何方法,那么缓存应该为空,如果此时需要插入缓存,肯定是需要开辟一块内存.

if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    // 获取旧的buckets
    bucket_t *oldBuckets = buckets(); 
    // 创建新的buckets
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this
    
    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    // 创建buckets和mask
    setBucketsAndMask(newBuckets, newCapacity - 1);
    // 清除旧的buckets
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
    }
}

通过代码可以看出,如果当前没有buckets,那么会通过cache_t::reallocate开辟一个大小为INIT_CACHE_SIZE内存(INIT_CACHE_SIZE为4),并且对mask复赋值,mask大小为当前可缓存方法数量-1。

1.2 newOccupied + CACHE_END_MARKER <= capacity / 4 * 3

查看newOccupied的定义:

mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;

可以看到,如果执行完方法后occupied小于原有大小的3/4,则继续下面流程。

1.3 新增后occupied大于原有3/4

capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; 
if (capacity > MAX_CACHE_SIZE) {
    capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);

如果新增后occupied大于原有大小的3/4,那么原有的buckets空间显然已经不适合存储现有方法,他会在原有大小的基础乘以2重新开辟一块空间,并将原有空间清空。同时,重新对mask赋值。

2 将方法缓存到buckets中

这个过程先通过计算得出当前方法的哈希值,然后通过哈希值查看对应位置的内存是否已被占用,如果没有,则缓存该方法,如果有并且当前位置缓存的方法不相同,则再次哈希找下一个位置保存方法。

从这个过程我们也可以得知,buckets实际是一张哈希表。

bucket_t *b = buckets();
mask_t m = capacity - 1;
// 计算当前方法哈希值
// 这里采用的哈希算法是 sel & mask
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(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)); // 当前方法发生哈希冲突,在哈希找位置

这也解释了为什么在一开始的示例中方法缓存并不是连续的。

总结

通过以上的探究,我们可以分析一下代码的执行结果

Animal *obj = [Animal alloc];
Class pClass = [Animal class];
    
struct lj_objc_class *lj_pClass = (__bridge struct lj_objc_class *)(pClass);
    
[obj func1];
[obj func2];
[obj func3];
[obj func4];

NSLog(@"occupied: %hu, mask: %u", lj_pClass->cache._occupied, lj_pClass->cache._mask);

for (mask_t i = 0; icache._mask; i++) {
    struct lj_bucket_t bucket = lj_pClass->cache._buckets[i];
    NSLog(@"%@ - %p", NSStringFromSelector(bucket._sel), bucket._imp);
}

当obj执行func3,缓存的方法数已经来到了3个,而此时的occupied已经是原有的3/4,所以需要缓存大小扩容到原来的2倍即8个bucket_t的大小,此时的mask为8-1=7,因为重新开辟空间所以缓存内原有的func1和func2被清空,最终缓存内只留下func3和func4,并且位置不定。

2020-09-18 14:27:25.269903+0800 cache_t分析[52626:5259235] -[Animal func1]
2020-09-18 14:27:25.269967+0800 cache_t分析[52626:5259235] -[Animal func2]
2020-09-18 14:27:25.270015+0800 cache_t分析[52626:5259235] -[Animal func3]
2020-09-18 14:27:25.270483+0800 cache_t分析[52626:5259235] -[Animal func4]
2020-09-18 14:27:25.270537+0800 cache_t分析[52626:5259235] occupied: 2, mask: 7
2020-09-18 14:27:25.270595+0800 cache_t分析[52626:5259235] (null) - 0x0
2020-09-18 14:27:25.270643+0800 cache_t分析[52626:5259235] (null) - 0x0
2020-09-18 14:27:25.270688+0800 cache_t分析[52626:5259235] (null) - 0x0
2020-09-18 14:27:25.270814+0800 cache_t分析[52626:5259235] func4 - 0x2fe8
2020-09-18 14:27:25.270867+0800 cache_t分析[52626:5259235] (null) - 0x0
2020-09-18 14:27:25.270945+0800 cache_t分析[52626:5259235] func3 - 0x2fb8
2020-09-18 14:27:25.270994+0800 cache_t分析[52626:5259235] (null) - 0x0

我们可以将cache的流程用下图描述:

cacheinsert流程图.png

你可能感兴趣的:(类结构探究(三)-- cache分析)