类的底层探究_cache

在之前的类的底层探究中, 我们并未对于类结构体中的cache_t进行详细说明,那么本节我们来看一下cache

一、cache的结构总览

 我们之前已经知道了cache占据了16个字节,那么我们来具体看一下cache,他本身存储了一个指针_bucketsAndMayBeMask是8个字节,还存储了一个联合体union,其中包含了_maybeMask(4字节)、_flags(2字节)、_occupied(2字节)、_originalPreoptcache(8字节指针)。

二、cache内部结构解析

LGPerson *p = [LGPerson alloc];
 [p method1];

我们简单的通过一行代码来找到通过类对象偏移16字节的地址打印出cache:

接着,我们对其中的属性逐个打印,但是无法获取我们想要的得到的存储方法的信息,因此我们从objc的源码出发探究cache的内部实现。我们从cache_t的方法中发现了一个insert方法,不难看出这个方法于插入方法有些关系。

接着,我们进入这个方法:

对于insert内部,我们可以看到是如何存放方法的,这里先对几个参数作简单说明:occuiped是数据占据大小,capacity是buckets的长度,buckets是存放数据的地方。

capacity是通过mask方法获取的,而mask其实是通过cache的成员变量_maybeMask获取的,capacity的长度等于_maybeMask+1,如果这里_maybeMask为空,则capacity为0。

bucket_t

我们先来看看buckets是如何获取的?他是通过cache的成员变量_bucketsAndMaybeMask获取的

接着,我们来看一下bucket_t的具体结构:bucket_t

存储是impsel,并且提供了set模版方法来存储存储impsel

最后,我们来总结一下cache_t内部各个属性的用处:

 

三、cache存入数据分析

当我们基本明白cache_t内部的结构之后,我们来分析一下cache是如何存储的:当某个方法需要被缓存时,需要先判断容量是否需要被扩容,然后进行存储缓存。

//cache缓存的是方法
    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    //cache_hash得到插入的位置
    mask_t begin = cache_hash(sel, m);
    //i不会超过capacty的长度-1
    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) {
            //如果已经缓存过了就直接return
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
        //如果存在hash冲突则重新计算哈希值
    } while (fastpath((i = cache_next(i, m)) != begin));

3.1 缓存存储

1.通过_bucketsAndMaybeMask来获取bucket_t容器(其实就是一个散列表)

bucket_t *b = buckets();

2.通过哈希算法算出当前需要存储的位置

  mask_t m = capacity - 1;

mask_t begin = cache_hash(sel, m);

首先根据buckets的长度-1来获取一个mask_t的值,然后用这个值计算存储位置的mask,然后通过cache_hash方法计算位置,cache_hash是通过mask & sel 来得到一个在【0,capacity】的索引:

3.根据索引判断该位置是否可以存储(等于0说明该位置没有被使用,可以被存储,否则该位置存储的方法与将要存储的方法相同)。

if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            //如果已经缓存过了就直接return
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }

4.如果出现了哈希冲突则需要解决,这里通过cache_next方法,对于cache_next方法不同的架构是不同的计算方式,在arm64架构下 如果当前位置为0 ,去判断 buckets的做后一个位置能否存储,如果不为0,则查看前面一个位置是否可以存储。

while (fastpath((i = cache_next(i, m)) != begin));

5.存储数据,如果找到可以存储的位置,则cache的_occupied+1,并通过bucket_t的set方法存储,并且重新将sel签名后存储。

if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }

3.2扩容

对于散列表存储数据,肯定会出现容量不够的情况,所以cache的缓存必然是需要处理容量的问题,下面就来分析一下在不同架构是如何进行扩容的,系统已经将这些差异定义为了宏定义。

#if __arm__  ||  __x86_64__  ||  __i386__
#define CACHE_END_MARKER 1
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}

#elif __arm64__ && !__LP64__
#define CACHE_END_MARKER 0
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}

#elif __arm64__ && __LP64__
#define CACHE_END_MARKER 0
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 7 / 8;
}

#define CACHE_ALLOW_FULL_UTILIZATION 1

#else
#error unknown architecture
#endif

enum {
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
    INIT_CACHE_SIZE_LOG2 = 2,
#else
support
    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),
};

整体上的扩容分为了四种情况,接下来,我们一一的看下:

1.cache为空:当cache为空,说明是第一次来到缓存数据,所以此时需要初始化一个INIT_CACHE_SIZE的buckets(以下我们简称为桶子);对于INIT_CACHE_SIZE 可以根据宏定义的得知,在arm64或M1下为 2 ,x86下为4。

if (!capacity) capacity = INIT_CACHE_SIZE;

2.看cache中占用的长度是否达到扩容的标准:判断是否需要扩容是通过newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)判断的

if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))
  • newOccupied = occupied + 1 即存储当前sel后占用的长度
  • cache_end_marker 在arm64或M1下的情况是0,在_x86_64下是1
  • cache_fill_ratio(capacity)他用来获取当前架构下存储数量扩容的标准,arm64/M1是capacity * 7/8 ;_x86_64架构下是capacity * 3/4

3.判断 CACHE_ALLOW_FULL_UTILIZATION 是否存满了数据:这个判断是在CACHE_ALLOW_FULL_UTILIZATION为真的条件下才会生效,在arm64CACHE_ALLOW_FULL_UTILIZATION为1,x86_64下为0

if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity)

CACHE_ALLOW_FULL_UTILIZATION系统中定义如下,也就是说这个值是 8;这里就是判断当容量小于 8的时候,查看存入当前 sel后是 是否超过 当前容量,如果超过就扩容

    FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
    FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),

4.如果CACHE_ALLOW_FULL_UTILIZATION为假,先计算扩容后的容量

  • 当存入当前的sel需要扩容时 ,此时就是把当前容量 * 2,如果计算后容量为0 ,则初始化为 INIT_CACHE_SIZE,并且保证 buckets的容量不会超过最大值 MAX_CACHE_SIZE = 1<<16
  • 重新创建  buckets,更新cache_bucketsAndMaybeMask_maybeMask,并释放旧的buckets
 else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

四、真机模拟cache流程

1.模拟数据结构:根据源码来仿写类的底层结构 和 cache的底层结构,通过类型强转将类对象转换为 仿写的类对象,然后读取cache的数据查看真机中缓存的数据,注意的是底层对于真机的架构下的宏定义。

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import <objc/message.h>
#import "LGPerson.h"

#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif


#define CACHE_END_MARKER 0
#define INIT_CACHE_SIZE 2

#define MAX_CACHE_SIZE_LOG2   16
#define MAX_CACHE_SIZE        (1 << MAX_CACHE_SIZE_LOG2)
//bucket_t源码模仿
struct  hw_bucket_t {
    IMP _imp;
    SEL _sel;
};

enum hw__legacy_memory_order {
    __mo_relaxed,
    __mo_consume,
    __mo_acquire,
    __mo_release,
    __mo_acq_rel,
    __mo_seq_cst
};

typedef enum hw_memory_order {
  memory_order_relaxed = __mo_relaxed,
  memory_order_consume = __mo_consume,
  memory_order_acquire = __mo_acquire,
  memory_order_release = __mo_release,
  memory_order_acq_rel = __mo_acq_rel,
  memory_order_seq_cst = __mo_seq_cst,
} memory_order;


//cache_t源码模仿
struct  hw_cache_t {
    uintptr_t _bucketsAndMaybeMask; // 8
    uint32_t   _maybeMask ;
    uint16_t    _flags ;
    uint16_t    _occupied ;
    
    // _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;
    
    
     hw_bucket_t *buckets() {
        return ( hw_bucket_t *)(_bucketsAndMaybeMask & bucketsMask);
    }
    
    uint32_t mask() const{
        uintptr_t maskAndBuckets = _bucketsAndMaybeMask;
        return maskAndBuckets >> maskShift;
    }
    
};

//class_data_bits_t源码模仿
struct  hw_class_data_bits_t {
    uintptr_t objc_class;
};

//类源码模仿
struct  hw_objc_class {
    Class isa;
    Class superclass;
    struct  hw_cache_t cache;
    struct  hw_class_data_bits_t bits;
};
 

2.模拟缓存方法的容量判断:根据源码中的条件判断,仿写判断,打印出当前cache的存储情况

void test(Class cls) {

    struct  hw_objc_class *pClass = (__bridge struct  hw_objc_class *)(cls);
    
    struct  hw_cache_t cache = pClass->cache;

    uintptr_t mask = cache.mask();
    
    uint32_t newOccupied = cache._occupied + 1;
    
    uintptr_t oldCapacity = mask ? (mask + 1) : 0, capacity = oldCapacity;
    
    // 第一次进入,还没有缓存方法
    if (cache._occupied == 0 ){
        capacity = INIT_CACHE_SIZE;
        NSLog(@"isConstantEmptyCache: oldOccupied == %d,nweOccupied ==%d ,oldCapacity == %lu, capacity == %lu" ,cache._occupied,newOccupied ,oldCapacity,capacity);
        
       // 判断是在   7 / 8 的范围内
    }else if (newOccupied + CACHE_END_MARKER <= (capacity * 7 / 8)){
        
        NSLog(@"cache_fill_ratio: oldOccupied == %d,nweOccupied ==%d ,oldCapacity == %lu, capacity == %lu" ,cache._occupied,newOccupied ,oldCapacity,capacity);
        
        //判读是容量是不是在 8以内且没存满
    } else if (capacity <= 8 && newOccupied + CACHE_END_MARKER <= capacity) {
        NSLog(@"CACHE_ALLOW_FULL_UTILIZATION: oldOccupied == %d,nweOccupied ==%d ,oldCapacity == %lu, capacity == %lu" ,cache._occupied,newOccupied ,oldCapacity,capacity);
    }else {
    // 需要扩容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //因为扩容了,所以下一个cache的Occupied 只有一个将要存进去的方法,所以此时 的 nweOccupied == 1
        NSLog(@"doublecapacity: oldOccupied == %d,nweOccupied ==%d ,oldCapacity == %lu, capacity == %lu" ,cache._occupied,1 ,oldCapacity,capacity);
    }
//    struct  hw_bucket_t * buckets = cache.buckets();
//
//    for (int i = 0; i < mask + 1; i++ ) {
//        SEL sel = buckets[i]._sel;
//        IMP imp = buckets[i]._imp;
//        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
//    }
    }

3.代码验证结果是否正确:由于系统中方法的调用是在 cache之后,所以在 调用方法前打印一次cache,模拟第一次缓存方法,bukets还不存在时的状况,即是 isConstantEmptyCache的情况,所以在打印的时候比较 nweOccupied 和 oldCapacity 查看是否需要扩容

        Person *p = [Person alloc];
        test(Person.class);
        [p instanceMetheod1];
        test(Person.class);
        [p instanceMetheod2];
        test(Person.class);
        [p instanceMetheod3];
        test(Person.class);
        [p instanceMetheod4];
        test(Person.class);
        [p instanceMetheod5];
        test(Person.class);
        [p instanceMetheod6];
        test(Person.class);
        [p instanceMetheod7];
        test(Person.class);
        [p instanceMetheod8];
        test(Person.class);
        [p instanceMetheod9];
        test(Person.class);
        [p instanceMetheod10];
        test(Person.class);
        [p instanceMetheod11];
        test(Person.class);
        [p instanceMetheod12];
        test(Person.class);
        [p instanceMetheod13];
        test(Person.class);
        [p instanceMetheod14];
        test(Person.class);
        [p instanceMetheod15];
        test(Person.class);
        [p instanceMetheod16];
        test(Person.class);
        [p instanceMetheod17];
        test(Person.class);
        [p instanceMetheod18];
        test(Person.class);
        [p instanceMetheod19];
        test(Person.class);
        [p instanceMetheod20];
        test(Person.class);
        [p instanceMetheod21];
        test(Person.class);
        [p instanceMetheod22];
        test(Person.class);
        [p instanceMetheod23];
        test(Person.class);
        [p instanceMetheod24];
        test(Person.class);
        [p instanceMetheod25];
        test(Person.class);
        [p instanceMetheod26];
        test(Person.class);
        [p instanceMetheod27];
        test(Person.class);
        [p instanceMetheod28];
        test(Person.class);
        [p instanceMetheod29];
        test(Person.class);
        [p instanceMetheod30];
        test(Person.class);
        
        

具体结果见:https://gitee.com/cike111/personal-notes/blob/master/iOS%20底层探究/%2005_%20iOS%20类的底层探究%20cache.md#获取bucket_t-容器--和-存储位置

五、思考与总结

5.1 为什么要释放掉旧的buckets

  • 由于哈希表的特性(地址映射),当每次总表扩容的时候,所有的元素映射都会失效,因为总容量变了,下标哈希结果也会改变
  • 如果需要之前所有的缓存的方法都重新存储,消耗和花费过大
  • 扩容是按照指数级增加的,如果及时清除,可以缓存更多的方法,减少扩容次数,从而提高效率

5.2 总结

  • cache_t内部使用了bucket_t这个容器中存储的sel 和imp
  • 当插入数据大于总容量的3/4(x86_64 / arm64 32位系统) || 7/8(真机 / arm64 64位系统)时,会对容器进行扩容处理
  • 真机在容量8以内会存满在扩容
  • 扩容是根据当前容量*2,按照指数级增加,需要将旧的缓存给释放掉,提高效率,减少消耗
  • bucket_t是一个散列表,通过sel&capacity-1哈希算法定位存储位置
  • 哈希冲突通过开放地址方式解决冲突 

 

posted on 2022-05-04 18:09  suanningmeng98  阅读(63)  评论(0)    收藏  举报