「OC」源码学习——cache_t的原理探究

发布于:2025-05-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

「OC」源码学习——cache_t的原理探究

前言

上一次我们讲到了objc_class的isa和bits之中的内容,接下来就是cache_t之中的内容。

cache_t探究

前面我们提到在类对象之中isasuperclass各占8个字节,所以我们可以直接通过内存平移获取相关内容,于是我们打开lldb继续调试

image-20250504114456081

好吧,看起来好像一点有用的看不到,没关系,我们接着查找到cache_t的定义

image-20250504114805185

可以看到在当前架构下,有用的属性为 _bucketsAndMaybeMask_unused_flags_occupied_originalPreoptCache,但由于_originalPreoptCache_unused_flags_occupied组成的结构体组成了联合体,由于联合体的互斥特性,我们可以先不看_originalPreoptCache_unused是占位符可以实现八位对齐

image-20250504123227932

通过源码的学习,我们可以知道这时候其实可以通过cache提供给的buckets()方法去获取_buckets,就可以获取sel-imp了,这两个的获取在bucket_t结构体中同样提供了相应的获取方法sel() 以及 imp(pClass),我们可以通过内存偏移的方法去获取第二个

image-20250504135605122

img

总结出cache_t的结构如上图

insert函数

image-20250504123246615

在运行完-[GGObject speak]方法之后,_occupied进行了自增,而调用-[GGObject sayHello]之后在查看发现_occupied又变成了1,这是为什么呢?

我们来看看cache_t之中关于insert的函数,就是将函数缓存插入bucket_t之中的实现

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    lockdebug::assert_locked(&runtimeLock);

    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }

    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif

    ASSERT(sel != 0 && cls()->isInitialized());

    // 缓存的容量管理
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {

        if (!capacity) capacity = INIT_CACHE_SIZE;//默认值为4
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // 缓存没超过3/4或者7/8
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
       // 对于小容量缓存(<=8),允许完全利用存储空间
    }
#endif
    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();//可以吧bucket_t理解为哈希表
    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<Atomic, Encoded>(b, 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));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}//哈希算法

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}//解决哈希冲突的方法


我们可以发现这个insert函数一共可以分为两个部分,一个是扩容判断,一个是插入缓存,在存储的时候使用哈希表,就是说明这些方法存放的地址并不一定连续。

我们可以看到_occupied代表的其实就是bucket进行内部存储的函数的个数,对于这个_occupied有几个点是值得注意的

  1. alloc申请空间时,此时的对象已经创建,如果再调用init方法,occupied也会+1
  2. 有属性赋值时,会隐式调用set方法,occupied也会增加,即有几个属性赋值,occupied就会在原有的基础上加几个
  3. 新的方法被调用时,occupied也会增加,即有几次调用,occupied就会在原有的基础上加几个

接着我们来理解的就是这个扩容操作

  • 首先如果缓存为空的话,就给buckets分配初始化的长度(x86_64为4,arm为2)并且创建一个buckets
  • 在arm64框架下,缓存的大小 <= buckets长度的7/8,并且buckets长度<=8没装满8,不扩容,在x86_64下,缓存的大小 <= buckets长度的3/4 ,不扩容。
  • 扩容逻辑:对当前容量的2倍扩容,并且如果扩容后容量大小 > MAX_CACHE_SIZE,则设置为MAX_CACHE_SIZE;计算出扩容大小后,以这个容量去创建新的buckets,和释放旧的buckets

这就是为什么当我们调用类之中-[GGObject sayHello]方法之后,反而_occupied变回了1,这就是因为旧值的buckets已经被完全释放,重新开辟了一个内存。

以下是开辟bucket内存reallocate方法的源码实现

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

_bucketsAndMaybeMask

ARM64 架构 下,_bucketsAndMaybeMask 被设计为 复合字段

  • 高位存储 mask(16 位):capacity - 1
  • 低位存储 buckets 地址(48 位):指向 bucket_t 数组的指针。(因为ARM64 架构下虚拟地址为48位)
mask_t cache_t::mask() const
{
    uintptr_t maskAndBuckets = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return maskAndBuckets >> maskShift;//maskShift大小为48位
}
struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}

程序可以通过以上形式分别获取mask和bucket。

参考文章

iOS-底层原理 11:objc_class 中 cache 原理分析

iOS底层-cache_t原理分析

Objective-C 类的底层探索