iOS底层代码探索004-由cache_t分析调用方法的缓存机制

1.前言

在003-类的底层探索中,我们研究了类里的bits里的内容。

superclass很明显是一个8字节的指向父类的指针。

那么cache里面存储的是什么呢?

今天,就让我们来研究一下cache_t。

首先计算偏移量=isa的大小+superclass的大小=8+8=16字节=0x10

 2.cache的基本数据结构

  

 

查看cache_t源码 

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;
    };
//......
    bool isConstantEmptyCache() const;
    bool canBeFreed() const;
    mask_t mask() const;
    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:
//....
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;
    mask_t occupied() const;
    void initializeToEmpty();
    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);
//....
};

_bucketsAndMaybeMask 变量与isa_t的bits类似,是一个指针类型存放地址。

联合体里有一个结构体和一个 结构体指针_originalPreoptCache。

结构体里有2/3个成员变量

  • explicit_atomic<mask_t>    _maybeMask,当前缓存区count,第一次开辟是3。
  • 如果是LP64(指的是Unix系统/类Unix系统 macOS也是)则有uint16_t  _flags;
  • uint16_t  _occupied:表示cache已存储的buckets数量,默认为0。

由于是联合体,结构体和 _originalPreoptCache是互斥的。

查看他的插入函数 寻找数据存储在哪里

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    //...........
    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;
    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);
}

可以发现它在一个bucket_t *b=buckets()获取到当前的bucket,然后进行插入。

查看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
//...
public:
    static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
//SEL
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
//...
    template <Atomicity, IMPEncoding>
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

里面存了imp和sel。imp和sel的关系如图所示:

可以发现是这样的一个关系

 

 3.LLDB调试验证

获取到TWPerson类的指针地址,加上偏移量0x10 

 

断点到这 TWPerson *p = [[TWPerson alloc] init];

再打印发现Value=3,occupied=2

 

 获取其bucket

 

 调用对象方法sayhi1,观察其变化,发现value=7,occupied=3

打印其buckets,发现buckets缓存了sayhi1。 

 

这里简单的说一下为啥buckets的存储插入是乱序的:

实际上这里的buckets使用的哈希表存储的方式,结合了数组和链表的优势,方便插入与查找。

并使用拉链法解决哈希冲突。 

4.脱离源码环境

 为了便于之后的探索,我们可以脱离源码环境,自己写出源码相似的结构,然后将程序内的cache_t强制转换成我们写的结构。结构体代码如下:

#import <Foundation/Foundation.h>
#import "TWTeacher.h"
#import <objc/runtime.h>
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
struct tw_cache_t {
    //uintptr_t _bucketsAndMaybeMask;//8
    struct tw_bucket_t *_buckets;
    mask_t   _maybeMask;//4
    uint16_t _flags;//2
    uint16_t _occupied;//2
};
struct tw_class_data_bits_t {
    uintptr_t bits;
};
struct tw_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct tw_objc_class {
    // 继承objc_object Class ISA;
    Class ISA;
    Class superclass;
    struct tw_cache_t cache;             // formerly cache pointer and vtable
    struct tw_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

main函数如下

/*
 解决问赋
 1.源码无法调试
 2.无需LLDB调试
 3.小规模取样 变得简单清晰
 a:1-3->1-7
 b:(null)->0x0 方法去哪
 c:2-7 +say4+没有类方法
 d:拿到了父类方法
 */
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TWPerson *p=[[TWPerson alloc]init];
        Class pClass=p.class;
        [p sayhi1];
        [p sayhi2];
        [p sayhi3];
        [p sayhi4];
        [p sayhi5];
        [p sayhi6];
        struct tw_objc_class *twclass=(__bridge struct tw_objc_class *)(pClass);
        for(mask_t i=0;i<twclass->cache._occupied;i++){
            struct tw_bucket_t bucket=twclass->cache._buckets[i];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }
        NSLog(@"%@",twclass);
        NSLog(@"%hu-%u",twclass->cache._occupied,twclass->cache._maybeMask);
    }
    return 0;
}

  

5.提出问题

  • 发现这里面消失了sayhi1。很明显是缓存机制,将sayhi1踢出了缓存,可明明是有7个位置,为什么没有存储sayhi1呢?
  • 我们之前LLDB调试也发现_maybeMask一开始从0,变成了3,之后又变成了7,这具体是怎么回事呢?

接下来我们进行cache_t的源码探索。

6.源码探索

 要想知道它为什么这么变化,我们得好好把插入sel和imp的insert函数给看懂了,一开始我给的省略了许多关键代码,接下来,我们一点点分析。

6.1 insert第一次插入情况

首先分析insert函数中的第一次插入情况,假设是我们是X86框架。

这里第一次,occupied()默认初始化为0,newOccupied=1,capacity=4;

查看reallocate函数

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);
    }
}

里面有个setBucketsAndMask函数 点进去发现不同架构有不同的操作,我们摘出x86的来分析。

#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;

可以看出,_bucketsAndMaybeMask中存储的内容是newMask和newBuckets,newBuckets是函数的第一个参数,是新开辟的内存空间地址,newMask是函数的第二个参数,即newCapacity-1,即是开辟容量-1,此时为3。

代码跑到这里,第一次cache_t插入时的初始化就完成了。

我们继续看插入,已注释。

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);
}

这里也解释了为什么buckets的下标是乱序的,以及如何解决的哈希冲突。

 接着我们看后续的插入,这里分3种情况(加一个初始化,insert里面就一共有1+3=4种情况)

6.2 insert后续插入情况分支1

static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}

 

newOccupied是已存储的buckets数量+1 再+1如果小于容量的3/4就无事发生,之后正常插入。

6.3 insert后续插入情况分支2

 

判断是否cache允许全部占据,是上一个3/4扩容的另一种情况,插满才扩容,实际上不会走这里。 

6.4 insert后续插入情况分支3 (扩容)

 此时扩容,扩容方式为两倍扩容,假设是第一次扩容,就是capacity=4*2=8。

此外还设置了cache的最大缓存容量:1<<16=2^16=65536。

    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),

 如果到了最大缓存容量,就停止扩容。

再使用reallocate函数,在6.1已经分析过了,实际上释放了之前创建的buckets,这就解释了为什么之前的sayhi1被抛弃了,实际上是因为重新申请了一个新的更大内存的buckets。

7.结尾

探索告一段落,如果任何问题,欢迎留言或联系我!谢谢观看。

8.参考文献

1.cache底层分析 https://www.jianshu.com/p/ced2e2ef7468

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值