美文网首页
7、Cache_t分析

7、Cache_t分析

作者: 白马啸红中 | 来源:发表于2020-12-17 19:55 被阅读0次
1、Cache_t结构

objc_class中除了isasuperclassbits三个重要的属性外还有一个重要的属性并未进行分析就是cache_t cache,看源码对这个属性的注释:

cache_t cache;             // formerly cache pointer and vtable

cache可谓是在计算机软硬件很普遍的存在,从物理的到虚拟的都存在这个概念,从计算机三级缓存到NSURLCache请求缓存,都是类似设计,所以这里的这个cache显然就是为了优化方法调用效率而使用的。
可以大致知道内部结构是大致存的是指针表结构。下面对源码结构进行解析:

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
    // 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.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _maskAndBuckets stores the mask shift in the low 4 bits, and
    // the buckets pointer in the remainder of the value. The mask
    // shift is the value where (0xffff >> shift) produces the correct
    // mask. This is equal to 16 - log2(cache_size).
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

    static constexpr uintptr_t maskBits = 4;
    static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
    static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

public:
    static bucket_t *emptyBuckets();
    
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    unsigned capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

#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

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void insert(Class cls, SEL sel, IMP imp, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn, cold));
};

结合一个类的代码:

@interface Person : NSObject
@property (nonatomic,assign)   int age;
@property (nonatomic,strong)   NSString *nickname;
@property (nonatomic,assign)   float height;
@property (nonatomic,strong)   NSString *name;

-(void)laugh;
-(void)cry;
-(void)run;
-(void)jump;
-(void)doNothing;
@end

@implementation Person
-(void)laugh
{
    NSLog(@"LMAO");
}

-(void)cry
{
    NSLog(@"cry me a river");
}

-(void)run
{
    NSLog(@"run! Forrest run!");
}

-(void)jump
{
    NSLog(@"you jump,I jump!");
}

-(void)doNothing
{
    NSLog(@"Today,I dont wanna do anything~");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool { 
        //main方法中
        Person *person =  [Person alloc];
        
        person.age = 10;
        person.nickname = @"pp";
        person.height = 180.0;
        person.name = @"ppext";
        
        [person laugh];
        [person cry];
        [person run];
        [person jump];
        [person doNothing];
        [person run];
        [person laugh];
        [person doNothing];
    }
    return 0;
}
@end

将断点断在倒数第二个方法,然后进行lldb调试,结合之前对bits的调试方法,可以减少16字节偏移,将指针指向cache

cache信息打印 这里可以确定cache_t的在基本属性i386x86_64架构下的属性是:
bucketsmaskflagsoccupied,和之前对cache_t所占内存大小的计算是一致的。其中后三个结构简单,但是不知道意思,第一个可以通过lldb配合cache_tbucket_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(SEL newSel, Class cls) const {
        return (uintptr_t)&_imp ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }

    // Sign newImp, with &_imp, newSel, and cls as modifiers.
    uintptr_t encodeImp(IMP newImp, 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(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:
    inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); }

    inline IMP rawImp(objc_class *cls) const {
        uintptr_t imp = _imp.load(memory_order::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(Class cls) const {
        uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order::memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(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(SEL newSel, IMP newImp, Class cls);
};

不管是哪种架构bucket_t中存的都有SELIMP,这个基本上就是方法的主要信息了。开始对里面信息打印:

buckets信息打印 既然是buckets绝对不只有一个,那就进行地址偏移打印一下:
地址偏移打印bucket信息 可以发现的是——没什么发现的-。- 这里打印即显示不出SEL中的方法名,也无法对应出IMP的地址,太抽象了。得来点可以打印调试的解决办法。
2、Cache_t的缓存机制
FBI WARNING:分析很长,结论也不短,如果不看分析可以直接跳最后总结。

书接上文,这里来个偷梁换柱的办法,将所有的这种类替换成自己的:

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

struct pp_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct pp_cache_t {
    struct pp_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct pp_class_data_bits_t {
    uintptr_t bits;
};

struct pp_objc_class {
    Class ISA;
    Class superclass;
    struct pp_cache_t cache;             // formerly cache pointer and vtable
    struct pp_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

将这段定义加入到源码之前,再修改后续代码,添加打印部分:

Person *person =  [Person alloc];
person.age = 10;
person.nickname = @"pp";
person.height = 180.0;
person.name = @"ppext";
        
[person laugh];
[person cry];
[person run];
[person jump];
[person doNothing];
        
struct pp_objc_class *pp_pClass = (__bridge struct pp_objc_class *)([Person class]);
NSLog(@"_occupied:%hu -_mask:%u",pp_pClass->cache._occupied,pp_pClass->cache._mask);
for (mask_t i = 0; i<pp_pClass->cache._mask; i++) {
            // 打印获取的 bucket
            struct pp_bucket_t bucket = pp_pClass->cache._buckets[i];
            NSLog(@"_sel:%@ - _imp:%p",NSStringFromSelector(bucket._sel),bucket._imp);
}
        
[person run];
[person laugh];
[person doNothing];

然后运行、打印一下:

自定义buckets的打印 这样就打印出现了SEL方法名和IMP地址了,而且occupied=2mask=15,而且还会出现空bucket。现象尽然出来了,那么问题就来了:
【一】occupied=2mask=15,这两个是什么意思,这个变化是有个什么规律?,打印的数目也是按照mask数目来打印的,多打印几个试试会不会有什么不一样?
【二】、之前学习操作系统就知道一种缓存的设计算法那就是内存分页或者缓存设计算法中LRU(最近最少使用),按照这样的话应该在jump后就是run方法,这里显然不是这种设计方式。所以这个算法是是需要研究的。
【三】、最早调用了laughcry方法呢,消失了,究竟去哪里了?

下面一个个来研究:
【一】、针对occupiedmask,在源码方法中只看到了两个的get方法:

mask_t cache_t::mask()
{
    uintptr_t maskAndBuckets = _maskAndBuckets.load(memory_order::memory_order_relaxed);
    uintptr_t maskShift = (maskAndBuckets & maskMask);
    return 0xffff >> maskShift;
}

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

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

mask具体什么值不知道,但是必定会是capacity+1,这个在reallocate的源码中也可以验证,尝试更改循环打印的数目,数目改多了会报错,所以这个就是mask应该就是capacity容量-1。下面就是occupied的了:
incrementOccupied方法、insert方法:
1、setBucketsAndMask中是将occupied设置为0。
2、incrementOccupied中只是单纯的occupied++
3、insertmask_t newOccupied = occupied() + 1也是+1。
其他的就不相干或者是旧版本的方法了,通过对比方法代码行数都可以知道,在insert方法中应该有部分流程,可以通过分析insert流程来确定两个值的意义。
insert方法的参数和实现可以知道这是对cache进行插入操作调用方法,因此通过Cache_t源码对这个大致流程分析以下:

ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
//首先
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif

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

首先,是一些判断和断言,不是重点,过~。

//然后
    // Use the cache as-is if it is less than 3/4 full
    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 <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

然后,是occupied直接加1,记为newOccupied,在cache空的时候,新申请内存,大小为capacity,如果cache不为空就判断newOccupied+1后是不是小于等于 capacity的3/4,小于的话直接走完判断,大于的话,在capacity不为0且时对capacity现有值翻倍,为0就将capacity重置为cache初始大小 1 << 2)即为0100=4,再判断capacity是否大于最大cache的大小——1 <<16=0x0001 0000 0000 0000 0000 即2的16次方,如果大于就设为最大值,并重新申请内存。

//最后
    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 because the
    // minimum size is 4 and we resized at 3/4 full.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(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));

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}

最后,重点是一个do-while循环:(由于这里是fastpath才能进入第二次循环,就按fastpath流程分析)
m = capacity - 1begin = cache_hash(sel, m)是一个位运算出来的到的值,sel & mask产生的,由于刚初始化所以ibegin等价。
do-while循环中,第一个判断,先确定i这个位置的bucketSEL是不是空,为空就表示这个位置是可以插入的,所以对occupied++,对这个位置的bucket设置SELIMP,且直接完成。第二个判断,要插入的SEL是否和当前位置ibucket相等,表示方法已经插入过,直接完成。循环判断条件,由于是模拟器运行,所以是x86分支,转换一下代码可以得到:(i = ((i+1)&m) != begin arm64也同样可以转化得到:(i = (i ? i-1 : m)) != begin,这里分析arm64流程,将如果i不为0就继续-1,所以在arm64的情况是从最大index慢慢循环到最小,也就是递减,为0就直接=m,也就和begin相等。
如果循环出来之后仍未插入bucket,就会走bad_cache方法。
分析完后发现,明面上除了当前位置ibucket为空了才进行incrementOccupied(),并未对mask的直接操作。
还有一个方法没有研究就是 reallocate方法,这里就不贴出源码了可以知道在reallocate方法中setBucketsAndMask方法中将occupied置为0。所以,捋一捋:

insert的流程 ,这里这个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) {
        cache_collect_free(oldBuckets, oldCapacity);
    }
}

参数:
1、mask_t oldCapacity旧的容量;
2、mask_t newCapacity新的容量;
3、bool freeOld是否释放旧的buckets
先将旧的buckets取出,然后以新的容量申请新的buckets内存并得到指针newBuckets
setBucketsAndMask是对内存的操作,将occupied置为0。
判断是否需要删除oldBuckets,需要就进入cache_collect_free方法。

static void cache_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_collect(false);
}

可以看出释放oldBuckets并不是直接置为nil,而是先使用_garbage_make_room装下来,然后通过cache_collect方法来将_garbage_make_room清空,大致流程就是这样。
那么这个insert的流程大致就清楚了,下面跟着lldb打印来验证一下(由于是在模拟器上调试的,所以部分分支走x86,而且之前在类的结构分析中打印methods是可以看到setget方法的,所以在这些自定义方法调用之前,必须先将set方法加入。这样走一边流程:

Step.1

调用setAge()方法——OldCapacity为0,newOccupied为1,——newOccupied + CACHE_END_MARKER=2>=0所以进入else——capacity=INIT_CACHE_SIZE=4,进入到reallocate方法,新申请capacity为4的buckets。进入循环,cache_hash(sel,m)得到ibegin。第一个条件b[i].sel() == 0由于cache是空的,所以必然会进入,所以occupied=1,将这个i = setAge&3这个位置设为setAge()方法的bucket,此时,occupied=1,mask=3。

Step.2

调用setNickname()方法——OldCapacity为4,newOccupied为2——newOccupied + CACHE_END_MARKER=3<=4 * 3/4——i=setNickname&3——进入循环,当前icry()方法的setNickname&3不同——occupied=2,将i = setNickname&3这个位置设为setNickname()方法的bucket——此时,occupied=2,mask=3。

Step.3

调用setHeight()方法——OldCapacity为4,newOccupied为3——newOccupied + CACHE_END_MARKER=4>4 * 3/4——进入else——capacity=capacity*2=8——进入reallocate——occupied置为0——释放oldBuckets——i=run&7——进入循环,当前isetHeight()方法的setHeight&7不同——occupied=1,将i = setHeight&7这个位置设为setHeight()方法的bucket——此时,occupied=1,mask=7。

Step.4

调用setName()方法——OldCapacity为8,newOccupied为2——newOccupied + CACHE_END_MARKER=3<=8 * 3/4——i=setName&3——进入循环,当前isetName()方法的setName&7不同——occupied=2,将i = setName&8这个位置设为setName()方法的bucket——此时,occupied=2,mask=7。

Step.5

调用laugh()方法——OldCapacity为8,newOccupied为3——newOccupied + CACHE_END_MARKER=4<=8 * 3/4——i=laugh&7——进入循环,当前ilaugh()方法的laugh&7不同——occupied=3,将i = laugh&7这个位置设为laugh()方法的bucket——此时,occupied=3,mask=7。

Step.6

调用cry()方法——OldCapacity为8,newOccupied为4——newOccupied + CACHE_END_MARKER=5<=8 * 3/4——i=cry&7——进入循环,当前icry()方法的cry&7不同——occupied=4,将i = cry&7这个位置设为cry()方法的bucket——此时,occupied=4,mask=7。

Step.7

调用run()方法——OldCapacity为8,newOccupied为5——newOccupied + CACHE_END_MARKER=6<=8 * 3/4——i=run&7——进入循环,当前irun()方法的run&7不同——occupied=5,将i = run&7这个位置设为run()方法的bucket——此时,occupied=5,mask=7。

Step.8

调用jump()方法——OldCapacity为8,newOccupied为6——newOccupied + CACHE_END_MARKER=7> 8 * 3/4——i=jump&7——进入else——capacity=capacity*2=16——进入reallocate——occupied置为0——释放oldBuckets——i=jump&15——进入循环,当前ijump()方法的jump&15不同——occupied=1,将i = jump&15这个位置设为jump()方法的bucket——此时,occupied=1,mask=15。

Step.9

调用doNothing()方法——OldCapacity为16,newOccupied为2——newOccupied + CACHE_END_MARKER=3<=16 * 3/4——i=doNothing&15——进入循环,当前idoNothing()方法的doNothing&15不同——occupied=2,将i = doNothing&15这个位置设为doNothing()方法的bucket——此时,occupied=2,mask=15。
至此,由于最近的一次reallocate是在jump方法,而且在x86模拟器环境下i值是递增的,所以在16个capacitynewBucket中最顶端的两个就是doNothingjump,之前的方法都被放进_garbage_make_room给释放了。

这样,之前存在的【一】【二】【三】三个问题就全部有了解释,在走了方法调用流程后就了解cache_t缓存的实现机制,\color{red}{but这样就完了吗?}
真相还不止这样,有很多细节都需要挖掘:
在循环的打印中修改一下打印内容,多加一个Person方便过滤:

NSLog(@"Person _occupied:%hu -_mask:%u",pp_pClass->cache._occupied,pp_pClass->cache._mask);
for (mask_t i = 0; i<pp_pClass->cache._mask; i++) {
            // 打印获取的 bucket
            struct pp_bucket_t bucket = pp_pClass->cache._buckets[i];
            NSLog(@"Person _sel:%@ - _imp:%p",NSStringFromSelector(bucket._sel),bucket._imp);
}

insert方法,do-while循环中加入打印:

do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
//加入打印
            printf("class:%s sel:%s i=%d capacity=%d occupied=%d\n",cls->mangledName(),(char *)sel,i,capacity,occupied());
            b[i].set<Atomic, Encoded>(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));

command+R跑起来,以Person为关键词过滤输出得到:

筛选打印 可以发现:
1、alloc方法也是走过insert的,这里我是没写init,其实init方法也会走insert,alloc方法通过注销代码调试一下,可以知道并没有进入buckets且也没有影响occupiedmask,但是init进入了,也会影响这两个值,甚至dealloccxx_destruct也进入insert了,这里就不研究了,不然本文更长了-。-。
2、capacityoccupiedmask的值如之前走流程一样没有问题,但是这个i是通过cache_hash生成的,是一种哈希算法,生成的第一个i值是不确定的,这主要是针对masksel,当然如果这两个都是一样那哈希生成的值也是确定的,这也是为什么程序运行多次,方法bucket的顺序还是不变的,这里jumpdoNothing的先后顺序就没那么重要了。

\color{red}{总结:}
1、Cache_t的结构:

//精简版
struct Cache_t {
    struct bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

//cache的主要缓存方法都在_buckets指针中
struct bucket_t {
    SEL _sel;
    IMP _imp;
};

2、Cache_t的缓存机制
2.1、容量capacity-1 = 掩码mask,当前存放的方法缓存occupied
2.2、容量capacity:初始容量为4,存放了大致超过3/4的量就会扩容,量翻倍;
2.3、当前存储bucketsoccupied:每次插入一个bucket就会加1,但是在扩容时会先置0再加1;
2.4、扩容:重新进行新内存的申请和就内存的释放,旧bucket会释放,新的bucket再插入buckets列表;
2.5、新bucket插入:以i为index,初始值是cache_hash方法以sel&mask哈希生成的随机值,然后以i = cache_next的方法进行遍历查找是否可以插入的位置i,直到可以插入为止occupied+1;(ps:找到相同sel就直接return)
2.6、allocinitdealloccxx_destruct等系统方法也会走缓存流程,但部分不会存入buckets列表,可以自己尝试。

至于insert方法的上层调用cache_fill是如何被调用的,需要结合runtime中的objc_msgSend流程,后续研究~

相关文章

网友评论

      本文标题:7、Cache_t分析

      本文链接:https://www.haomeiwen.com/subject/lrehgktx.html