iOS底层原理之cache底层详解

120 阅读6分钟

本文主要内容

1.通过源码分析cache的缓存内容
2.cache扩容的引出
3.cache_t解析
4.cache扩容规则解析
5.真机测试cache缓存规则

本节内容较难理解,请大家仔细阅读结合源码多看几遍,认真理解!

一、通过源码分析cache的缓存内容

类对象的objc_class结构体中包括ISAsuperclasscachebits这几个成员,在前几篇文章中,我们对ISA有了一定的了解,也研究了包含实例方法、属性、协议等内容bits成员,下面我们来研究该结构体中的另一个成员:cache
查看objc4-838.1源码,如果熟读前面的文章,我们很容易知道,类对象的内存地址向右平移16个字节即可得到cache的地址image.png

找到cache的内存地址,通过强转为cache_t类型并读取cache的数据内容。

image.png 找到cache_t的源码发现打印的内容即为其结构体的成员。但仍然无法了解这些成员的含义。根据前面的经验,我们去分析cache_t提供的方法。

image.png 经过查看cache_t提供的方法,找到一个叫作insert的函数。在insert方法中注意到bucket_t *b = buckets(),而bucket_t存储的内容就是方法名和方法的实现。接着是对对象b进行set操作,操作对象b时使用数组的格式b[i]调用,所以要分析i如何取值。

image.png

image.png

现在开始分析i的取值,首先计算mask_t m = capacity - 1;,已知_maybeMask = bucket_t长度-1(后续详述),所以capacity = oldCapacity =capacity() = mask()+1 =_maybeMask+1=bucket_t长度-1+1= bucket_t,所以m的值为bucket_t-1

image.png

image.png

image.png

接着计算mask_t begin = cache_hash(sel, m);cache_hash算法函数中,先把sel转换成uintptr_t类型即unsigned long类型的值value(该数字很大)。因此value&mask的结果最大值是mask。所以begin的值最大值就是mbucket_t-1

image.png

由此得出i的值不会超过bucket_t-1

验证把"sel"转换成uintptr_t类型即unsigned long类型的数字value很大!
复制代码

image.png

上述的set操作实际上就是对方法的存储。为什么说对对象b进行set操作实际上就是对方法的保存?在set方法中,分别调用了_imp_store和_sel_store函数。

image.png

为什么会找到insert的方法?首先cache表示缓存,所以肯定会涉及到插入、删除等操作,这样就顺其自然的关注到这个方法.各位读者经过长期跟随作者的底层源码分析,也会逐渐形成这样的举一反三的思维能力!
复制代码

总结整个过程:通过buckets()获取到bucket_t *类型的b,确定b的取值范围i最大为bucket_t长度-1,判断通过哈希算法得到的i对应的b[i]的sel是否有值,如果没有值,通过set函数将imp和sel存入b[i]中;如果b[i]的sel是否等于当前要存储的sel,如果相同说明已经缓存,直接返回;如果b[i]的sel有值并且不等于当前要存储的sel则调用 cache_next函数,当前要存储的sel存储到下标为i+1的对象中。

由此得出结论:cache是用来缓存方法的,即sel和imp!

二、cache扩容的引出

既然cache是用来缓存方法的,那么cache把方法存储在什么地方呢? 直接读取cache中的成员发现,方法sel和imp并无法获取到。

image.png

根据经验会想到去找函数来获取。由一、通过源码分析cache的缓存内容中发现,存储方法selimp的是一个bucket_t的结构体类型。在源码中找到返回bucket_t类型数据的buckets()函数,调用该函数并读取返回的内存内容,发现确实是存储了selimp,首先存储有class方法。

image.png

image.png 继续读取,发现存储了respondsToSelector:方法。

image.png

三.cache_t解析

cache_t结构体中包含_bucketsAndMaybeMask(保证对数据增删改查时线程安全)占用8字节,还有1个联合体,其中包含4个成员:mask_t类型即unsigned int_maybeMask占用4个字节、uint16_t类型即unsigned short类型的_flags占用2个字节、uint16_t类型即unsigned short类型的_occupied占用2个字节、preopt_cache_t *(指针)类型的_originalPreoptCache占用8个字节,所以cache_t结构体总共为16字节!所以,可以验证通过类对象内存平移16个字节即可找到cache得内存地址

image.png

四.cache扩容规则解析

二、cache扩容的引出中分析方法的存储时,调用方法method1后,可能只会找到方法class和方法respondsToSelector的存储而找不到method1。这是因为cache进行了扩容。 进入之前研究的insert函数中,在存储方法的selimp之前,即bucket_t *b = buckets();之前实现了cache扩容的逻辑。

image.png

mask_t newOccupied = occupied() + 1;开始分析,occupied()返回cache_t的成员变量_occupied,当第一次进入insert函数时,_occupied为0也就是occupied()为0,所以newOccupied值为1;oldCapacity等于bucket_t的长度。

首次调用会进入第一个ifif (slowpath(isConstantEmptyCache()))),如图中1: INIT_CACHE_SIZEx86_64架构下为1<<2(4),arm64架构下1<<1(2),即capacity在在x86_64架构下为4,arm64架构下2,再调用reallocate函数判断是否需要释放oldBuckets。即第一次调用insert时,在x86_64架构下开辟一个长度为4的桶子,在arm64架构下开辟一个长度为2的桶子。

如图中2cache_fill_ratiox86_64架构下为bucket_t长度的3/4,在arm64架构下为bucket_t长度的7/8。所以如果缓存的大小在x86_64架构下小于等于桶子长度的3/4,在arm64架构下小于桶子长度的7/8,在arm64架构下桶子长度小于等于8时不做任何操作。

否则,如图中3,会进行扩容。释放oldBuckets. image.png

image.png

总结:cache扩容规则

  • 在x86_64架构下,当缓存的大小等于桶子长度的3/4的时候,进行两倍扩容;
  • 在arm64架构下,当缓存的大小大于桶子长度的7/8的时候,进行两倍扩容;桶子长度小于等于8时不会扩容。

疑难问题:cache扩容逻辑需要根据源码多次分析理解!

五.真机测试cache缓存规则

通过仿写源码中的objc_class结构体,其中也包括仿写ISAsuperclasscachebits这几个成员,利用仿写的objc_class结构体对Class进行转换,这样就可以获取到仿写的cache中的数据。如下为部分代码供参考:

typedef uint32_t mask_t// x86_64 & arm64 asm are less efficient

//preopt_cache_entry_t源码模仿

struct lg_preopt_cache_entry_t {

    uint32_t sel_offs;

    uint32_t imp_offs;

};

//preopt_cache_t源码模仿

struct lg_preopt_cache_t {

    int32_t  fallback_class_offset;

    union {

        struct {

            uint16_t shift       :  5;

            uint16_t mask        : 11;

        };

        uint16_t hash_params;

    };

    uint16_t occupied    : 14;

    uint16_t has_inlines :  1;

    uint16_t bit_one     :  1;

    struct lg_preopt_cache_entry_t entries;

    

    inline int capacity() const {

        return mask + 1;

    }

};

//bucket_t源码模仿

struct lg_bucket_t {

    IMP _imp;

    SEL _sel;

};

//cache_t源码模仿

struct lg_cache_t {

    uintptr_t _bucketsAndMaybeMask; // 8

    struct lg_preopt_cache_t _originalPreoptCache; // 8

    

    // _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;

    

    static constexpr uintptr_t preoptBucketsMarker = 1ul;



    // 63..60: hash_mask_shift

    // 59..55: hash_shift

    // 54.. 1: buckets ptr + auth

    //      0: always 1

    static constexpr uintptr_t preoptBucketsMask = 0x007ffffffffffffe;

    

    lg_bucket_t *buckets() {

        return (lg_bucket_t *)(_bucketsAndMaybeMask & bucketsMask);

    }

    

    uint32_t mask() const {

        return _bucketsAndMaybeMask >> maskShift;

    }

    

};

//class_data_bits_t源码模仿

struct lg_class_data_bits_t {

    uintptr_t objc_class;

};


//类源码模仿

struct lg_objc_class {

    Class isa;

    Class superclass;

    struct lg_cache_t cache;

    struct lg_class_data_bits_t bits;

};

复制代码

如上通过对源码的仿写,对cache扩容规则进行了验证,即在arm64架构下(iOS为arm64架构),当缓存的大小大于桶子长度的7/8的时候,进行两倍扩容;桶子长度小于等于8时不会扩容!

本文总结

1.cache是用来缓存方法的,缓存使方法的调用更高效;
2.在x86_64架构下,当缓存的大小等于桶子长度的3/4的时候,进行两倍扩容;
3.在arm64架构下,当缓存的大小大于桶子长度的7/8的时候,进行两倍扩容;桶子长度小于等于8时不会扩容。