objc_class 中 cache 原理分析

380 阅读9分钟

cache_t结构

以下是类在底层的结构

截屏2021-06-22 下午3.24.20.png

其中cache_t的结构如下

/**
 
 架构 : arm64
 模拟器 : i386
 mac : x86_64
 
 */
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
    };

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    // _bucketsAndMaybeMask is a buckets_t pointer
    // _maybeMask is the buckets mask

    static constexpr uintptr_t bucketsMask = ~0ul;
    static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
    
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
    static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
#endif
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
    // _maybeMask is unused, the mask is stored in the top 16 bits.

    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;

    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;

    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    
    // Ensure we have enough bits for the buckets pointer.
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS,
            "Bucket field doesn't have enough bits for arbitrary pointers.");

#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
#if __has_feature(ptrauth_calls)
    // 63..60: hash_mask_shift
    // 59..55: hash_shift
    // 54.. 1: buckets ptr + auth
    //      0: always 1
    static constexpr uintptr_t preoptBucketsMask = 0x007ffffffffffffe;
    static inline uintptr_t preoptBucketsHashParams(const preopt_cache_t *cache) {
        uintptr_t value = (uintptr_t)cache->shift << 55;
        // masks have 11 bits but can be 0, so we compute
        // the right shift for 0x7fff rather than 0xffff
        return value | ((objc::mask16ShiftBits(cache->mask) - 1) << 60);
    }
#else
    // 63..53: hash_mask
    // 52..48: hash_shift
    // 47.. 1: buckets ptr
    //      0: always 1
    static constexpr uintptr_t preoptBucketsMask = 0x0000fffffffffffe;
    static inline uintptr_t preoptBucketsHashParams(const preopt_cache_t *cache) {
        return (uintptr_t)cache->hash_params << 48;
    }
#endif
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _bucketsAndMaybeMask is a buckets_t pointer in the top 28 bits
    // _maybeMask is unused, the mask length is stored in the low 4 bits

    static constexpr uintptr_t maskBits = 4;
    static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
    static constexpr uintptr_t bucketsMask = ~maskMask;
    static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#else
#error Unknown cache mask storage type.
#endif

    bool isConstantEmptyCache() const;
    bool canBeFreed() const;
    mask_t mask() const;

#if CONFIG_USE_PREOPT_CACHES
    void initializeToPreoptCacheInDisguise(const preopt_cache_t *cache);
    const preopt_cache_t *disguised_preopt_cache() const;
#endif

    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);

    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);

    static bucket_t *emptyBuckets();
    static bucket_t *allocateBuckets(mask_t newCapacity);
    static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));

public:
    // The following four fields are public for objcdt's use only.
    // objcdt reaches into fields while the process is suspended
    // hence doesn't care for locks and pesky little details like this
    // and can safely use these.
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;

#if CONFIG_USE_PREOPT_CACHES
    const preopt_cache_t *preopt_cache() const;
#endif

    mask_t occupied() const;
    void initializeToEmpty();

#if CONFIG_USE_PREOPT_CACHES
    bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = (uintptr_t)&_objc_empty_cache) const;
    bool shouldFlush(SEL sel, IMP imp) const;
    bool isConstantOptimizedCacheWithInlinedSels() const;
    Class preoptFallbackClass() const;
    void maybeConvertToPreoptimized();
    void initializeToEmptyOrPreoptimizedInDisguise();
#else
    inline bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = 0) const { return false; }
    inline bool shouldFlush(SEL sel, IMP imp) const {
        return cache_getImp(cls(), sel) == imp;
    }
    inline bool isConstantOptimizedCacheWithInlinedSels() const { return false; }
    inline void initializeToEmptyOrPreoptimizedInDisguise() { initializeToEmpty(); }
#endif

    void insert(SEL sel, IMP imp, id receiver);
    void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
    void destroy();
    void eraseNolock(const char *func);

    static void init();
    static void collectNolock(bool collectALot);
    static size_t bytesForCapacity(uint32_t cap);

#if __LP64__
    bool getBit(uint16_t flags) const {
        return _flags & flags;
    }
    void setBit(uint16_t set) {
        __c11_atomic_fetch_or((_Atomic(uint16_t) *)&_flags, set, __ATOMIC_RELAXED);
    }
    void clearBit(uint16_t clear) {
        __c11_atomic_fetch_and((_Atomic(uint16_t) *)&_flags, ~clear, __ATOMIC_RELAXED);
    }
#endif

#if FAST_CACHE_ALLOC_MASK
    bool hasFastInstanceSize(size_t extra) const
    {
        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        }
        return _flags & FAST_CACHE_ALLOC_MASK;
    }

    size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

    void setFastInstanceSize(size_t newSize)
    {
        // Set during realization or construction only. No locking needed.
        uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;
        uint16_t sizeBits;

        // Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16
        // to yield the proper 16byte aligned allocation size with a single mask
        sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
        sizeBits &= FAST_CACHE_ALLOC_MASK;
        if (newSize <= sizeBits) {
            newBits |= sizeBits;
        }
        _flags = newBits;
    }
#else
    bool hasFastInstanceSize(size_t extra) const {
        return false;
    }
    size_t fastInstanceSize(size_t extra) const {
        abort();
    }
    void setFastInstanceSize(size_t extra) {
        // nothing
    }
#endif
};

其中的_bucketsAndMaybeMask is a buckets_t pointer,是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

    // Compute the ptrauth signing modifier from &_imp, newSel, and cls.
    uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
        return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }

    // 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
    }

public:
    static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }

#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
    inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
        return (IMP)imp;
    }

    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
    }

    template <Atomicity, IMPEncoding>
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

从以上bucket_t的属性和方法中可以看出它应该与imp有联系——事实上bucket_t作为一个桶,里面是用来装imp方法实现以及它的key. 所以通过上面两个结构体源码可知,而我们cache中缓存的正好是sel-imp. 整体的结构如下图所示

9f8236219b2945bdb86303b219998169~tplv-k3u1fbpfcp-watermark.image.jpeg

在cache中查找sel-imp

  • 通过源码查找 -- LLDB调试
  • 脱离源码在项目中查找 创建一个LGPerson类,并定义一个属性及一个实例方法及其实现

3A249F67-321B-445C-A955-88B2DAF67ABE.png

lldb调试
(lldb) p/x pClass
(Class) $0 = 0x0000000100008430 LGPerson
(lldb) p (cache_t *)0x0000000100008430
(cache_t *) $1 = 0x0000000100008430
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4295001096
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 3580224
        }
      }
      _flags = 1
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x000000010036a140
      }
    }
  }
}
(lldb) p [p sayHello]
2021-06-28 09:35:47.301192+0800 KCObjcBuild[1254:26104] -[LGPerson sayHello]
(lldb) p pClass
(Class) $3 = LGPerson
(lldb) p/x pClass
(Class) $4 = 0x0000000100008430 LGPerson
(lldb) p (cache_t *)0x0000000100008430
(cache_t *) $5 = 0x0000000100008430
(lldb) p 0x0000000100008430+0x10
(long) $6 = 4295001152
(lldb) p *$6
error: <user expression 8>:1:1: indirection requires pointer operand ('long' invalid)
*$6
^~~
(lldb) p/x 0x0000000100008430+0x10
(long) $7 = 0x0000000100008440
(lldb) p *$7
error: <user expression 10>:1:1: indirection requires pointer operand ('long' invalid)
*$7
^~~
(lldb) p (cache_t *)0x0000000100008440
(cache_t *) $8 = 0x0000000100008440
(lldb) p *$8
(cache_t) $9 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4302790864
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000007
      }
    }
  }
}
(lldb) p $8.buckets()
(bucket_t *) $10 = 0x00000001007760d0
  Fix-it applied, fixed expression was: 
    $8->buckets()
(lldb) p $10.sel()
(SEL) $11 = (null)
  Fix-it applied, fixed expression was: 
    $10->sel()
(lldb) p $10.imp(nil,pClass)
(IMP) $12 = 0x0000000000000000
  Fix-it applied, fixed expression was: 
    $10->imp(nil,pClass)
(lldb) p *$10
(bucket_t) $13 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $8.buckets()[1]
(bucket_t) $14 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 47104
    }
  }
}
  Fix-it applied, fixed expression was: 
    $8->buckets()[1]
(lldb) p $14.imp(nil,pClass)
(IMP) $15 = 0x0000000100003c30 (KCObjcBuild`-[LGPerson sayHello])
  • cache属性的获取,需要通过pclass的首地址平移16字节(类中isa指针占8字节,superclass指针占8字节),即首地址+0x10获取cache的地址

  • 从源码的分析中,我们知道sel-imp是在cache_t的_buckets属性中(目前处于macOS环境),而在cache_t结构体中提供了获取_buckets属性的方法buckets()

  • 获取了_buckets属性,就可以获取sel-imp了,这两个的获取在bucket_t结构体中同样提供了相应的获取方法sel() 以及 imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls).

由上图可知,在没有执行方法调用时,此时的cache是没有缓存的,执行了一次方法调用,cache中就有了一个缓存,即调用一次方法就会缓存一次方法.

脱离源码在项目中查找
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct kc_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct kc_cache_t {
    struct kc_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

struct kc_class_data_bits_t {
    uintptr_t bits;
};

// cache class
struct kc_objc_class {
    Class isa;
    Class superclass;
    struct kc_cache_t cache;             // formerly cache pointer and vtable
    struct kc_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = p.class;  // objc_clas
        [p say1];
        [p say2];
        [p say3];
        [p say4];
        [p say1];
        [p say2];
//        [p say3];

        [pClass sayHappy];
        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
        // 0 - 8136976 count
        // 1 - 3
        // 1: 源码无法调试
        // 2: LLDB
        // 3: 小规模取样
        
        // 底层原理
        // a: 1-3 -> 1 - 7
        // b: (null) - 0x0 方法去哪???
        // c: 2 - 7 + say4 - 0xb850 + 没有类方法
        // d: NSObject 父类
        
        for (mask_t i = 0; i<kc_class->cache._maybeMask; i++) {
            struct kc_bucket_t bucket = kc_class->cache._bukets[i];
            NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
        }
        NSLog(@"Hello, World!");
    }
    return 0;
}

这里有个问题需要注意,在源码中,objc_class的ISA属性是继承自objc_object的,但在我们将其拷贝过来时,去掉了objc_class的继承关系,需要将这个属性明确,否则打印的结果是有问题,如下图所示,

86407C6B-8E7C-41D1-9B06-51B935DC0760.png

加上ISA属性后,增加两个方法的调用,其正确的打印结果应该是这样的

8A4048D7-140B-4239-BEE4-C13008077C0A.png

在增加两个方法的调用,即解开say3、say4的注释,其打印结果如下

86BA0641-8CC0-41FD-858A-B290678F6FFF.png

针对上面的打印结果,有以下几点疑问

  1. _mask是什么?
  2. _occupied 是什么?
  3. 为什么随着方法调用的增多,其打印的occupied 和 mask会变化?
  4. bucket数据为什么会有丢失的情况?,例如2-7中,只有say3、say4方法有函数指针
  5. 2-7中say3、say4的打印顺序为什么是say4先打印,say3后打印,且还是挨着的,即顺序有问题?
  6. 打印的cache_t中的_ocupied为什么是从2开始?

带着上述的这些疑问,下面来进行cache底层原理的探索

cache_t底层原理分析

  • 首先,从cache_t中的_mask属性开始分析,找cache_t中引起变化的函数,发现了incrementOccupied()函数

8BEA05B5-1343-4998-AC0D-3485DBF81560.png 该函数的具体实现为

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

  • 源码中,全局搜索incrementOccupied()函数,发现只在cache_t的insert方法有调用

D01D39C1-5C58-442D-A302-0331C23A8511.png

  • insert方法,理解为cache_t的插入,而cache中存储的就是sel-imp,所以cache的原理从insert方法开始分析,以下是cache原理分析的流程图

2c6b02d3f23b4608a3765da51899eb50~tplv-k3u1fbpfcp-watermark.image.jpeg

  • 全局搜索cache_t::insert,发现在写入之前,还有一步操作,即cache读取,即查找sel-imp,如下所示

EE70A9B0-0D0F-4AE0-928A-5745B4BFEB14.png

insert方法分析

在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; // 1+1
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//4
        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 {// 4*2 = 8
        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; // 4-1=3
    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
}

主要分为以下几部分

  • 【第一步】计算出当前的缓存占用量
  • 【第二步】根据缓存占用量判断执行的操作
  • 【第三步】针对需要存储的bucket进行内部imp和sel赋值
【第一步】计算出当前的缓存占用量

根据occupied的值计算出当前的缓存占用量,当属性未赋值及无方法调用时,此时的occupied()为0,而newOccupied为1,如下所示 mask_t newOccupied = occupied() + 1;

关于缓存占用量的计算,有以下几点说明:

  • alloc申请空间时,此时的对象已经创建,如果再调用init方法,occupied也会+1
  • 当有属性赋值时,会隐式调用set方法,occupied也会增加,即有几个属性赋值,occupied就会在原有的基础上加几个
  • 当有方法调用时,occupied也会增加,即有几次调用,occupied就会在原有的基础上加几个
【第二步】根据缓存占用量判断执行的操作
  • 如果是第一次创建,则默认开辟4个
  • 如果缓存占用量小于等于3/4,则不作任何处理
  • 如果缓存占用量超过3/4,则需要进行两倍扩容以及重新开辟空间
realloc方法:开辟空间

该方法,在第一次创建以及两倍扩容时,都会使用 主要有以下几步

  • allocateBuckets方法:向系统申请开辟内存,即开辟bucket,此时的bucket只是一个临时变量
  • setBucketsAndMask方法:将临时的bucket存入缓存中,此时的存储分为两种情况:
    • 如果是真机,根据bucket和mask的位置存储,并将occupied占用设置为0
    • 如果不是真机,正常存储bucket和mask,并将occupied占用设置为0
  • 如果有旧的buckets,需要清理之前的缓存,即调用collect_free方法该方法的实现主要有以下几步:
    • _garbage_make_room方法:创建垃圾回收空间
      • 如果是第一次,需要分配回收空间
      • 如果不是第一次,则将内存段加大,即原有内存*2
    • 记录存储这次的bucket
    • cache_collect方法:垃圾回收,清理旧的bucket
【第三步】针对需要存储的bucket进行内部imp和sel赋值

这部分主要是根据cache_hash方法,即哈希算法 ,计算sel-imp存储的哈希下标,分为以下三种情况:

  • 如果哈希下标的位置未存储sel,即该下标位置获取sel等于0,此时将sel-imp存储进去,并将occupied占用大小加1

  • 如果当前哈希下标存储的sel 等于 即将插入的sel,则直接返回

  • 如果当前哈希下标存储的sel 不等于 即将插入的sel,则重新经过cache_next方法 即哈希冲突算法,重新进行哈希计算,得到新的下标,再去对比进行存储

其中涉及的两种哈希算法,其源码如下

  • cache_hash:哈希算法
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask; // 通过sel & mask(mask = cap -1)
}

  • cache_next:哈希冲突算法
#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask; //(将当前的哈希下标 +1) & mask,重新进行哈希计算,得到一个新的下标
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask; //如果i是空,则为mask,mask = cap -1,如果不为空,则 i-1,向前插入sel-imp
}

cache_t疑问点

为什么是在 3/4 时进行扩容

在哈希这种数据结构里面,有一个概念用来表示空位的多少叫做装载因子——装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降

负载因子是3/4的时候,空间利用率比较高,而且避免了相当多的Hash冲突,提升了空间效率