objc_class(cache探索)

在objc_class结构体中,有一个cache_t类型,在这篇内容中,主要探索这个内容存储的数据,以及存储的规律。老样子通过内存位移先得到cache_t的内存地址。


cache_t的打印结果,看起来没有特别清楚的字段,去cache_t结构体里看看

在源码中,没具体找到相关信息,但是看到了insert方法,

我猜测插入方法,应该就是加入相应的方法缓存,因为有sel和imp,我们可以根据这个insert方法进行追踪,点进去看实现找到了一个buckts,方法内部就是操作这个buckts。


buckts

而cache_t有一个buckets()方法,进行打印
buckets打印结果

老样子,暂时看不出什么有用的信息,就去bucke_t结构体里面去瞧瞧

看一下这个capacity

unsigned oldCapacity = capacity(), capacity = oldCapacity;
capacity方法实现

继续跳下去


mask方法

那么_maybeMask = capacity - 1

那我们就继续打印一下buckts方法,看看里面的内容。


打印buckets方法

bucket_t中的sel和imp方法

继续打印


sel和imp

老规矩,看到不太明白的数据直接去源码中查找有没有对应的方法
找到了对应的sel和imp

我们可以通过查看sel方法来得到当前bucket_t指针数组中的存储内容,因为是一个指针数组类型,所以我们可以通过地址+1的方法,获取所有的存储内容.因为是散列表存储的,所以可能不连续,所以第一个+1的地址没有存储方法。
打印

打印

可以通过打印结果来看,bucket里面存储了一个class方法和一个respondsToSelector方法,既然是缓存方法,那么再调用方法之后,应该也可以打印出来,调用method1。


method1之后打印缓存

从图片来看打印结果,method1不仅没有出现,respondsToSelector也消失不见了。这就是因为缓存扩容的原因,旧的bucket被释放了,储存的数据也释放了。我们来探究一下cache扩容。

cache扩容

在insert方法中,如果缓存超出一定的临界值,就会进行扩容。


cache扩容方法

occupied()方法

可以看出返回的是cache_t的成员变量_occupied,那么首次,_occupied=0,即newOccupied = 1;

//capacity方法实现
unsigned cache_t::capacity() const
{
    return mask() ? mask()+1 : 0; 
}

//mask方法实现
mask_t cache_t::mask() const
{//_maybeMask = bucket_t的长度-1
    return _maybeMask.load(memory_order_relaxed);
}

oldCapacity = capacity();capacity = _maybeMask + 1;而 _maybeMask = bucket长度- 1;即capacity = bucket长度。

  • 反过来看第一个条件判断
if (slowpath(isConstantEmptyCache())) {//当缓存为空的时候
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//赋一个INIT_CACHE_SIZE
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }

//跳一下 INIT_CACHE_SIZE
enum {
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
    // When we have a cache end marker it fills a bucket slot, so having a
    // initial cache size of 2 buckets would not be efficient when one of the
    // slots is always filled with the end marker. So start with a cache size
    // 4 buckets.
    INIT_CACHE_SIZE_LOG2 = 2,//
#else
    // Allow an initial bucket size of 2 buckets, since a large number of
    // classes, especially metaclasses, have very few imps, and we support
    // the ability to fill 100% of the cache before resizing.
    INIT_CACHE_SIZE_LOG2 = 1,
#endif
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
    FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
    FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};

//

MAX_CACHE_SIZE_LOG2的值依赖于CACHE_END_MARKER,而CACHE_END_MARKER:在x86_64下位1,在arm64下值为0,所以MAX_CACHE_SIZE_LOG2在x86_64下位2,在arm64下为1
INIT_CACHE_SIZE在x86_64下1<<2 = 4;在arm64下1<<1为2,即bucket_t初始化的长度,在arm64家沟下位2,在x86_64下为4;初始长度计算结束后,然后调用了reallocate(oldCapacity, capacity, false); //false是是否释放掉旧的bucket
reallocate实现

ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = 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);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);//第一次没有旧的buckets,所以不用释放
    }
}

那么这个条件判断,我们就可以理解为在arm64架构下开辟一个长度为2的bucket,在x86_64架构下开辟一个长度为4的bucket。

  • 再看第二个条件判断
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }

//cache_fill_ratio方法如下图
cache_fill_ratio

由图可得,在x86_64架构下为bucket长度的四分之三,而CACHE_END_MARKER在x86_64下=1,计算基数会+1,即长度为4的情况下,第三个方法就会存进新的筒子里;
在arm64架构下为bucket长度的八分之七,即当长度为16的时候,第十五个数据会进行扩容。
也就是这个条件判断可以理解为,在arm64架构下,如果缓存的大小小于等于bucket长度的八分之七,在x86_64架构下如果缓存的大小小于等于bucket长度的四分之三,则什么也不干。

  • 第三个条件
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
FULL_UTILIZATION_CACHE_SIZE

FULL_UTILIZATION_CACHE_SIZE 等于1<<3=8
即在arm64架构下,当筒子的长度小于等于8的时候,什么都不做

  • 最后扩容写法
else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

上述条件都不满足的时候,直接两倍扩容,最大值是MAX_CACHE_SIZE = 1<<16,最后reallocate传的是true,就是将旧bucket释放掉,原来在旧bucket存的值就不存在了

整个cache扩容的规则

  • 在x86_64架构下,当缓存的大小等于bucket长度的四分之三的时候(会进行+1),进行两倍扩容
  • 在arm64架构下,当缓存的大小小于等于bucket长度的半分之七的时候,进行两倍扩容,当bucket长度小于等于8的时候,不满不扩容
  • 扩容后,释放旧bucket,新bucket不存储旧bucket内容

那根据这个扩容规则,电脑架构是x86,开头遇见的问题在执行method1方法后,bucket里面只有一个class,是因为在执行method的时候,进行了扩容,所以只有一个class方法。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容