10.Class-cache_t

之前进行类的结构的分析的时候,我们还有一个重要的东西没有进行研究,就是 cache_t

一、cache_t 的基本结构

和之前一样,我们在源码环境下编译调试。

回顾一下class的基础结构:

-
ISA
superClass
cache
bits

说明

ISA(结构体指针)

父类(结构体指针)

方法缓存

类的具体信息

大小(字节)

8

8

16

8

1.1 源码

其核心数据结构如下

struct cache_t {

// <<<< 主要数据结构
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
    ...
};
  • _bucketsAndMaybeMask

    • _bucketsAndMaybeMask is a buckets_t pointer

    • 它是一个 buckets_t 类型的指针

  • _maybeMask

    • _maybeMask is the buckets mask

  • _occupied

  • _originalPreoptCache

1.2 LLDB调试

我们无法直接输出相关成员内容了,为了继续输出相关属性我们继续看源码

相关get方法

我们使用 buckets() 方法继续输出调试

这里你会发现用到了 $12.buckets()[2] 的下标是2,0和1都是空的。因为这里 buckets 不是数组,是哈希表的结构。

1.3、bucket_t的数据结构

包含了 SELIMP

二、自己实现 cache_t 数据结构

使用LLDB进行调试感觉到太麻烦了,为了更方便的进行调试我们尝试对数据结构进行模仿。

2.1 调试代码

输出:

2.2 扩容

我们调整一下代码

我们发现这里进行了扩容。这里又是什么逻辑呢?

三、insert 与扩容

为了更好的理解扩容,我们先了解一下方法缓存是怎么插入的。

3.1 核心源码解读

a. INIT_CACHE_SIZE 初始化缓存

通过源码发现,这里是通过位运算进行的缓存空间大小初始化。

如,当前我们的是 1 << 2 = 4。同时我们还能发现最大值为 1 << 16

b. 扩容临界点

newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)

// Cache is less than 3/4 or 7/8 full. Use it as-is.

真机为 7/8

c. 扩容操作

扩容按 x2 进行扩容

d. 扩容后

我们看一下触发扩容的那次Log:

这次扩容后,我们发现。除了最新的,之前的缓存都没有了。

这是因为 扩容 并不是真正意义上的扩展之前的空间,而是重新开辟一块空间。

3.2 cache_hash 计算哈希下标

3.3 cache_next 寻找插入位置

3.4 set 差入逻辑

将方法插入缓存,并不是直接插入的。而是进行了编码。(uintptr_t)newImp ^ (uintptr_t)cls;

3.5 流程图

cache_t

关于类型转换的思考

这里使用自己定义的类型进行转换,感觉是不是很懵,感觉这也行?

我们抛开代码,思考一下:我们为什么要定义各种数据结构?为了能读写数据。

而能正确读写数据的前提是,计算机能认识这些数据。而我们定义的数据结构,就是计算机认识他们的基准。

只要两个数据结构的内存结构一致,那么进行转换就没有问题。

参考

HashMap的负载因子为什么默认是0.75

Last updated

Was this helpful?