OC底层探究之类的cache_t分析

487

一、cache数据结构

1、通过源码分析cache

《OC底层探究之类的底层原理结构》一文中,我们已经知道了类的结构,并且分析了bits

现在开始分析cache,cache顾名思义就是缓存

依旧是objc源码,打上断点,通过内存平移0x10,得到cacheimage-20210623150644273

发现_maybeMask_occupied的值都为0,而其他的值都比较大,其内部的结构和我们之前查阅源码是一致的:

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;
    };
    //省略方法、全局变量等等,方法在方法区、全局变量在全局区不占用结构体内存
}

__LP64__是指Unix和Unix类的系统(Linux,Mac OS X等等)。

但是cache_t里面这么多成员变量,到底是哪个才是缓存呢?

缓存不外乎就是增删改查,在方法里面找相关方法!

cache_t这个结构体里面找到了insert方法,很明显这就是插入方法:

void insert(SEL sel, IMP imp, id receiver);

进入到insert方法里面,看方法先看return

		// Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    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));

发现returnb[i]调用了set方法,那么b是什么呢:

bucket_t *b = buckets();

再进入到bucket_t结构体:

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<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
//省略方法等
}

发现bucket_t就有IMPSEL这2个成员变量。

SEL : 类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。

IMP:一个函数指针,保存了方法的地址

bucket_t会在哪个成员变量里面呢?

很明显在_bucketsAndMaybeMask里面!

2、图形总结

那么我们就可以得出一个这样的图:

buckets

二、cache底层LLDB分析

接下来我们打印一下_bucketsAndMaybeMask

(lldb) p $2._bucketsAndMaybeMask
(explicit_atomic<unsigned long>) $3 = {
  std::__1::atomic<unsigned long> = {
    Value = 4436931456
  }
}

发现里面有个Value,继续打印Value

(lldb) p $3.Value
error: <user expression 4>:1:4: no member named 'Value' in 'explicit_atomic<unsigned long>'
$3.Value
~~ ^

获取不到数据怎么办?

找方法!

cache_t结构体里面就会发现:

struct bucket_t *buckets() const;

接着调用buckets()方法:

(lldb) p $2.buckets()
(bucket_t *) $4 = 0x0000000108763380
(lldb) p *$4
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}

这就打印出了我们的_sel_imp

但是没有值!

因为对象没有使用方法,所以没有缓存!

先使用方法后再去获取:

image-20210623155359379

这次有值了,而且cache_t里面的_maybeMask的值为3_occupied的值为1,和我们的方法数量有点相像,但是SEL和我们获取的的方法名好像不一样!

接下来继续去bucket_t里面查找相应的方法:

inline SEL sel() const { return _sel.load(memory_order_relaxed); }

inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

可以找到SELIMP相应的获取方法!

继续打印:

(lldb) p $4.sel()
(SEL) $5 = "printName"

但是imp方法需要传bucket_tClassbucket_t不知道怎么办?

nil进行尝试:

(lldb) p $4.imp(nil, pClass)
(IMP) $6 = 0x0000000100003d70 (HObjectBuild`-[HPerson printName])

成功打印了我们使用的方法printName

三、脱离源码分析cache

1、自定义源码结构体

如果没有源码的情况呢?我们该怎么去调试呢?

只需要自己去定义相关的结构体即可!

开一个新工程,先自定义objc_class结构体,并重命名:

struct h_objc_class {
   	Class isa;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

然后发现cache_tclass_data_bits_t找不到,继续自定义cache_t结构体:

struct h_cache_t {    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;    };};

然后发现explicit_atomic是未知的,所以我们直接删除。

mask_t也不知道,去源码查看:

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

所以直接把mask_t也拿过来!

下面的联合体只需要结构体内的即可,所以可以把_originalPreoptCache删除,并把结构体拿出来:

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

struct h_cache_t {
    uintptr_t _bucketsAndMaybeMask;
    
    mask_t    _maybeMask;
#if __LP64__
    uint16_t  _flags;
#endif
    uint16_t  _occupied;
};

继续根据源码自定义class_data_bits_t

struct h_class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
};

然后friend这个友类就不需要了:

struct h_class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
};

最后再调整h_objc_class结构体:

struct h_objc_class {
   	Class isa;
    Class superclass;
    struct h_cache_t cache;             // formerly cache pointer and vtable
    struct h_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

这样我们的自定义objc_class结构体就完成了!

最后,我们还需要bucket_t这个结构体,按照上面的做法:

struct h_bucket_t {
    // 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__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif
};

同时可以把uintptr_t换为_imp的真正类型IMP

struct h_bucket_t {
    // 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__
    IMP _imp;
    SEL _sel;
#else
    SEL _sel;
    IMP _imp;
#endif
};

这样自定义源码的结构体就基本完成了!

2、调试

把类强转为自定义的h_objc_class结构体:

struct h_objc_class * pClass = (__bridge struct h_objc_class *)(p.class);

再打印我们之前感觉和方法数量相像的成员变量的值:

image-20210623221027880

打印出了13,和之前用lldb一样!

接下来就要找到_bucketsAndMaybeMask里的bucket_t

我们先看看buckets()方法:

struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}

打上断点,发现addr_bucketsAndMaybeMask的值是一样:

image-20210623230436978

说明load就是获取了_bucketsAndMaybeMask的值,然后在& bucketsMask,就得到了bucket_t的指针地址!

所以我们可以直接把_bucketsAndMaybeMask换成bucket_t结构体指针:

struct h_cache_t {
    struct h_bucket_t * _buckets;
    
    mask_t    _maybeMask;
#if __LP64__
    uint16_t  _flags;
#endif
    uint16_t  _occupied;
};

这样就不用再去写buckets()相关方法了!可以省略很多繁琐的步骤!

然后就去获取bucket_t结构体了:

image-20210624092945044

成功的打印出了我们使用过的方法!

但是如果有多个方法缓存呢?有该怎么获取呢?

buckets()方法很明显会返回多个bucket!但是根据源码返回的是指针,所以我们是否可以用指针偏移去获取其他的bucket呢?

假设cache_t里面的_occupied存储的是方法数量,for循环打印:

image-20210624111214507

发现并没有完全打印出我们使用过的方法!

那我们试试_maybeMask

image-20210624111318931

这回就成功的打印出了我们使用过的所有方法!

3、总结

这样就完全脱离了源码,并且调试成功,而且不需要lldb调试,方便修改,简单清晰!

如果有时候下载的源码因为种种原因无法调试的话,可以尝试使用此方法!

以及需要小规模取样的时候也可以尝试使用此方法!

四、cache底层原理分析

1、问题

刚刚我们明明只有调用了2个方法,但是却在打印出一个方法后才能完全打印出来!

  • 那么如果我们有3个方法呢?

image-20210624140600774

就会发现,首先occupied变成了1maybeMask变成了7,而且只打印出了最后一个方法!并且有大量的null

  • 如果有类方法呢?

image-20210624140743428

好像类方法并没有在缓存里面!

  • 如果调用了init方法呢?

image-20210624140931242

发现init方法进入了缓存,但是我们并没有重写init方法啊!这是为什么呢?

2、底层分析

①整理思路

首先先分析_bucketsAndMaybeMask

explicit_atomic<uintptr_t> _bucketsAndMaybeMask;

很明显_bucketsAndMaybeMask里面存储着是bucketsMaybeMask

但是他的值却只有1个,是怎么存储的呢?

根据之前的对buckets()方法的探索,我们可以推断出_bucketsAndMaybeMaskisa类似,也是在位上有存储信息!取信息的时候就进行&操作!

那么要怎么验证呢?

首先找到切入点!cache是缓存,必然有也有

所以从开始探索!

②纵观全局

重新探索cache_t结构体里的insert方法:

void insert(SEL sel, IMP imp, id receiver);

insert方法传了3个参数,SEL方法编号,IMP方法地址,receiver接收者即调用者!

进入到insert方总览:

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

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

    // Use the cache as-is if until we exceed our expected fill ratio.
    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 <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#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
    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.
    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
}

③首次执行

1、先看2个赋值:

// Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;

先是创建了一个newOccupied,即已使用的缓存数量,再看看occupied()方法:

mask_t cache_t::occupied() const
{
    return _occupied;
}

发现就获取_occupied

在来看看capacity()方法:

unsigned cache_t::capacity() const
{
    return mask() ? mask()+1 : 0; 
}

mask_t cache_t::mask() const
{
    return _maybeMask.load(memory_order_relaxed);
}

这里oldCapacitycapacity都为_maybeMask的值 + 1,或为0!

2、看判断逻辑:

如果是第一次缓存,则进入空缓存判断:

if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }

slowpath:大概率不会执行

fastpath:大概率会执行

首次插入_maybeMask0,所以capacity被赋值为INIT_CACHE_SIZE,进入INIT_CACHE_SIZE

/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
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),
};

可以得出INIT_CACHE_SIZE1左移2位,即为4,所以capacity4

在看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,再创建新的buckets,然后去设置BucketsAndMask

先看allocateBuckets方法:

bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
    // Allocate one extra bucket to mark the end of the list.
    // This can't overflow mask_t because newCapacity is a power of 2.
    bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);

    bucket_t *end = endMarker(newBuckets, newCapacity);

#if __arm__
    // End marker's sel is 1 and imp points BEFORE the first bucket.
    // This saves an instruction in objc_msgSend.
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
    // End marker's sel is 1 and imp points to the first bucket.
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
    
    if (PrintCaches) recordNewCache(newCapacity);

    return newBuckets;
}

这里是调用了calloc方法,即开辟了内存,看看bytesForCapacity方法:

size_t cache_t::bytesForCapacity(uint32_t cap)
{
    return sizeof(bucket_t) * cap;
}

因为参数newCapacity4bucket_t里面为SELIMP,所以开辟了4 * 16 = 64字节内存!

在看end,调用了endMarker方法:

bucket_t *cache_t::endMarker(struct bucket_t *b, uint32_t cap)
{
    return (bucket_t *)((uintptr_t)b + bytesForCapacity(cap)) - 1;
}

这里是通过内存平移得到开辟的内存的最后16字节内存地址

然后在end处插入了一个1,以及新Buckets首地址

再进入到setBucketsAndMask方法:

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    // objc_msgSend uses mask and buckets with no locks.
    // It is safe for objc_msgSend to see new buckets but old mask.
    // (It will get a cache miss but not overrun the buckets' bounds).
    // It is unsafe for objc_msgSend to see old buckets and new mask.
    // Therefore we write new buckets, wait a lot, then write new mask.
    // objc_msgSend reads mask first, then buckets.

#ifdef __arm__
    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();

    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);

    // ensure other threads see new buckets before new mask
    mega_barrier();

    _maybeMask.store(newMask, memory_order_relaxed);
    _occupied = 0;
#elif __x86_64__ || i386
    // ensure other threads see buckets contents before buckets pointer
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);

    // ensure other threads see new buckets before new mask
    _maybeMask.store(newMask, memory_order_release);
    _occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}

先是_bucketsAndMaybeMasknewBuckets转为指针地址存入,再是_maybeMask存入newMasknewMasknewCapacity - 1newCapacity4,所以_maybeMask3

这也是为什么只有1个缓存方法的时候_maybeMask3的原因!

同时这时newBuckets里还没有方法,所以_occupied0

3、方法插入Buckets中:

		bucket_t *b = buckets(); //取出刚刚存入的bucket_t
    mask_t m = capacity - 1; //4 - 1 = 3
    mask_t begin = cache_hash(sel, m); //通过运算得到开始位置
    mask_t i = begin;
    
//----以下是cache_hash方法----
// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

然后进行查找空位进行插入操作:

// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
    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));

先进入do,看第一个if,就是取刚刚创建的bucket_t里的sel,然后判断sel是否有值,因为是刚创建的所有大多数情况没有值!进入第一个if

再看incrementOccupied()方法:

void cache_t::incrementOccupied() 
{
    _occupied++;
}

这就是_occupied++!所以确定_occupied为方法缓存数量!

然后就调用bucket_t的set方法传入bselimpcls

template<Atomicity atomicity, IMPEncoding impEncoding>
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls)
{
    ASSERT(_sel.load(memory_order_relaxed) == 0 ||
           _sel.load(memory_order_relaxed) == newSel);

    // objc_msgSend uses sel and imp with no locks.
    // It is safe for objc_msgSend to see new imp but NULL sel
    // (It will get a cache miss but not dispatch to the wrong place.)
    // It is unsafe for objc_msgSend to see old imp and new sel.
    // Therefore we write new imp, wait a lot, then write new sel.
    
    uintptr_t newIMP = (impEncoding == Encoded
                        ? encodeImp(base, newImp, newSel, cls)
                        : (uintptr_t)newImp);

    if (atomicity == Atomic) {
        _imp.store(newIMP, memory_order_relaxed);
        
        if (_sel.load(memory_order_relaxed) != newSel) {
#ifdef __arm__
            mega_barrier();
            _sel.store(newSel, memory_order_relaxed);
#elif __x86_64__ || __i386__
            _sel.store(newSel, memory_order_release);
#else
#error Don't know how to do bucket_t::set on this architecture.
#endif
        }
    } else {
        _imp.store(newIMP, memory_order_relaxed);
        _sel.store(newSel, memory_order_relaxed);
    }
}

先是创建newIMP,判断是否需要编码

如果不用编码则直接赋值传进来的imp

如果需要编码则进行编码encodeImp

// Sign newImp, with &_imp, newSel, and cls as modifiers.
    uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        return (uintptr_t)
            ptrauth_auth_and_resign(newImp,
                                    ptrauth_key_function_pointer, 0,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

编码的本质就是将impclass进行异或操作!

编码完成后判断是否是原子操作!

是的话判断当前imp是否和新imp一样,不一样才进行存储!

不是的话直接存储!

第二个if就是判断如果bucket_t里面的sel和当前的sel一样则不用处理!

再进入到while

先看cache_next方法:

#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
#else
#error unexpected configuration
#endif

依旧是通过运算获取hash地址!找到空位再执行do语句!

④再次执行

回到开头的判断:

		// Use the cache as-is if until we exceed our expected fill ratio.
    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 <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#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
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

//----以下为cache_fill_ratio方法----
// Historical fill ratio of 75% (since the new objc runtime was introduced).
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}

第二次执行的时候,newOccupied_occupied + 1 = 2capacity_maybeMask + 1 = 4

newOccupied + CACHE_END_MARKER = 2 + 1 = 3;
cache_fill_ratio(capacity) = cache_fill_ratio(4) = 3;

即进入到第二个if,直接进入后面的代码!

所以第二次执行的时候后面的和第一次一样,在bucket_t里面插入新的方法即可!

⑤第三次执行

第三次执行的时候,newOccupied_occupied + 1 = 3capacity_maybeMask + 1 = 4

newOccupied + CACHE_END_MARKER = 3 + 1 = 4;
cache_fill_ratio(capacity) = cache_fill_ratio(4) = 3;

正常情况,即进入到最后一个else,执行reallocate方法!

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

reallocate方法里面,重新创建新的bucket_t!因为参数传了true,所以释放旧的bucket_t

/***********************************************************************
* cache_t::collect_free.  Add the specified malloc'd memory to the list
* of them to free at some later point.
* size is used for the collection threshold. It does not have to be 
* precisely the block's size.
* Cache locks: cacheUpdateLock must be held by the caller.
**********************************************************************/
void cache_t::collect_free(bucket_t *data, mask_t capacity)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif

    if (PrintCaches) recordDeadCache(capacity);

    _garbage_make_room ();
    garbage_byte_size += cache_t::bytesForCapacity(capacity);
    garbage_refs[garbage_count++] = data;
    cache_t::collectNolock(false);
}

这里调用了系统的垃圾栈方法继续释放内存!

而且在setBucketsAndMask方法中会把_occupied赋值为0_maybeMask赋值为7

所以当我们执行了3个方法的时候,_occupied1_maybeMask为7。

因为旧方法被释放,只有新方法被存入!所以会有大量的null

为什么要把旧的方法释放,不移到新的内存空间呢?因为开辟内存并不是在原有的内存地址进行扩张,而是重新开辟新的内存空间!而把旧方法移到新的内存空间会非常消耗性能,所以直接释放是最高效的!

五、图形梳理

insert