OC 类原理探索:cache 的结构分析

OC 类原理探索 系列文章

  1. OC 类原理探索:类的结构分析
  2. OC 类原理探索:类结构分析补充
  3. OC 类原理探索:属性的底层原理
  4. OC 类原理探索:cache_t 的结构分析

前言

先回顾下的结构:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;           
    class_data_bits_t bits; 
}
复制代码

isasuperclassbits在前几篇文章中都有聊过,cache是方法的缓存,今天来对cache的结构进行探索。

准备工作

一、cache 的基本数据结构

打开 objc4-818.2 源码,查看cache_t的数据结构:

struct cache_t {
    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
    };
}
复制代码

关于__LP64__我们可以看下面这张表:

image.png

根据表格可以看出,__LP64__Mac OS X中是有效的,__LP64__等于1,所以cache结构中有_flags这个成员变量。

lldb 探索 cache 结构

lldb通过地址偏移找到cache

image.png

cache我们虽然找到了,但是并没有找到类似list的这种和方法列表有关的变量,所以我们返回源码去寻找和列表有关的方法

源码找方法

image.png

我们看到了emptyBuckets()清空方法和bucket_t的相关操作,点击查看
bucket_t

image.png

最终找到了存储方法的数据结构bucket_tbucket_t中有_sel_imp两个成员变量。

SEL 和 IMP 的关系

  • SEL方法编号,相当于书本目录的名称;
  • IMP函数指针地址,相当于书本目录的页码;
  • 函数实现相当于书本中具体的内容;
  • 查找过程
    • 首先明白我们要找到书本的什么内容:SEL目录里面的名称;
    • 然后通过目录名称找到对应的页码:IMP
    • 通过页码去定位具体的内容。

image.png

cache 的大体结构

探索到这里,可以得出cache的大体结构如下:

image.png

二、cache 底层 LLDB 分析

我们继续上面的lldb探索,在cache_t中找到buckets()函数:

struct cache_t {
    ...
    struct bucket_t *buckets() const;
    
    struct bucket_t *cache_t::buckets() const
    {
        uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
        return (bucket_t *)(addr & bucketsMask);
    }
    ...
}
复制代码

bucket_t中可以找到sel()函数和imp()函数:

struct bucket_t {
    ...
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    
    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
    }
    ...
}
复制代码

通过lldb打印cache中缓存的方法:

image.png

  • 通过上面lldb调试,打印buckets()[2]得到了缓存方法say1
  • 但是buckets()[0]buckets()[1]是空值;
  • _maybeMask的值是3_occupied的值是1

为什么会有这样的结果呢,我们接下来继续调试。

三、脱离源码 分析

  • 我们之前一直都是用lldb的方式来探索底层,接下来换一种方式来分析;
  • 新的方式是:创建和底层源码一样的数据结构,来进行底层的探索分析。

仿照底层源码,创建数据结构

typedef uint32_t mask_t;

struct ssl_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct ssl_cache_t {
    struct ssl_bucket_t *_buckets; // 8
    mask_t    _maybeMask; // 4
    uint16_t                   _flags;  // 2
    uint16_t                   _occupied; // 2
};

struct ssl_class_data_bits_t {
    uintptr_t bits;
};

struct ssl_objc_class {
    Class isa;
    Class superclass;
    struct ssl_cache_t cache;
    struct ssl_class_data_bits_t bits;
};
复制代码

脱离源码 打印分析

添加下面的代码,用来打印_maybeMask_occupied方法缓存列表:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        SSLPerson *p = [SSLPerson alloc];
        Class pClass = p.class;
        
        struct ssl_objc_class *ssl_class = (__bridge struct ssl_objc_class *)(pClass);
        NSLog(@"%hu - %u",ssl_class->cache._occupied,ssl_class->cache._maybeMask);

        for (mask_t i = 0; i < ssl_class->cache._maybeMask; i++) {
            struct ssl_bucket_t bucket = ssl_class->cache._buckets[i];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }
    }
    return 0;
}
复制代码

下面打印测试几种情况:

1. 调用say1方法

image.png

  • 打印结果
    • _maybeMask的值是3_occupied的值是1
    • 成功打印了say1方法。
  • 为什么会有这样的结果呢,后面进行分析。

2. 调用say1say2say3say4方法

image.png

  • 打印结果
    • _maybeMask的值是7_occupied的值是2
    • 只打印了say4say3两个方法。
  • 为什么会有这样的结果呢,后面进行分析。

3. 调用say1say2say3

image.png

  • 打印结果
    • _maybeMask的值是7_occupied的值是1
    • 只打印了say3方法。
  • 为什么会有这样的结果呢,后面进行分析。

4. 初始化时调用init方法

image.png

  • 打印结果
    • init方法是可以正常打印;
    • _maybeMask的值是3_occupied的值是1
  • 结果分析
    • init是对象方法;
    • [SSLPerson alloc] = p,所以可以打印。

5. 调用+ (void)sayHeiHei类方法

image.png

  • 打印结果
    • _maybeMask_occupied的值都是0
  • 结果分析
    • 因为sayHeiHei是类方法,存储在元类中,我们打印的缓存方法是找不到的。

为什么 脱离源码分析

  • 这份代码可以跑在任何的工程中,不需要源码环境,非常的方便;
  • lldb调试过程中,遇到问题要重新输出地址,重新跑一遍;
  • 有的时候源码无法调试,也就没法lldb调试;
  • 小规模取样,去除多余代码,可以对底层结构更加的清晰。

四、cache 底层原理分析

  • _bucketsAndMaybeMask我们可以理解为uintptr_t类型,为8字节,存储这bucketsMaybeMask
  • 接下来去探索怎么得到这两个值,我们以插入为切入点进行分析,也就是写入数据的时候。

insert 函数解析

catch_t中找到insert()函数,和用到的相关函数:

void insert(SEL sel, IMP imp, id receiver);

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    ...
    mask_t newOccupied = occupied() + 1; // _occupied+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;
    
    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);
}

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

struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

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

static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}
复制代码

第一次插入方法时,进入insert函数:

  • insert函数传了selimpreceiver三个参数;
  • mask_t newOccupied = occupied() + 1;
    • mask_t32位整形;
    • occupied()就是返回_occupied的值,因为是第一次进入所以函数值为0
    • 所以第一次时newOccupied = 0 + 1 = 1;
  • if (slowpath(isConstantEmptyCache()))()
    • 因为是第一次进入,没存过值,所以表达式为true,进入执行体。
  • if (!capacity) capacity = INIT_CACHE_SIZE;
    • INIT_CACHE_SIZE的值为4,所以capacity = 4
  • reallocate(oldCapacity, capacity, /* freeOld */false);
    • 传入capacity4,但是开辟了3字节空间;
    • 可下面查看reallocate函数的解析,再回来继续向下看。
  • bucket_t *b = buckets();
    • 获取当前buckets容器;
  • mask_t m = capacity - 1;
    • m = 4 - 1 = 3
  • mask_t begin = cache_hash(sel, m);
    • 有三个内存不知道存在哪儿里,通过cache_hash得到哈希地址,就是开始的存储的位置begin;
  • 接下来do while循环
    • 无限去找,这个位置有了吗没有,cache_next()((i+1) & mask)去找下一个;
    • 进行再哈希,防止哈希冲突,直到不等于begin,开始插;
    • 因为是哈希,所以上面打印方法时,方法不是连续的
  • fastpath(b[i].sel() == 0)
    • 从来没有进来过,第一次,开始incrementOccupied()_occupied++;,
    • 所以第一次打印occupied = 1
  • b[i].set<Atomic, Encoded>(b, sel, imp, cls());
    • 在合适的位置插入值;
  • b[i].sel() == sel
    • 如果已经存在了就不插了;
  • bad_cache(receiver, (SEL)sel);
    • 如果do while循环最终没有找到合适的位置,进行bad_cache

第二次插入方法时,进入insert函数:

  • mask_t newOccupied = occupied() + 1; // 1+1
    • newOccupied = 1 + 1 = 2
  • unsigned oldCapacity = capacity(), capacity = oldCapacity;
    • capacity = oldCapacity = mask() ? mask()+1 : 0; = 3 + 1 = 4
  • else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))
    • cache_fill_ratio(capacity)capacity75%,所以值为3
    • 如果超过75%会进行扩容;
    • 2 + 1 <= 3,表达式成立,执行体什么也没有做。
  • 继续执行上面说过的插入操作,不再赘述。

第三次插入方法时,进入insert函数:

  • mask_t newOccupied = occupied() + 1; // 2+1
    • newOccupied = 2 + 1 = 3
  • unsigned oldCapacity = capacity(), capacity = oldCapacity;
    • capacity = oldCapacity = mask() ? mask()+1 : 0; = 3 + 1 = 4
  • else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))
    • cache_fill_ratio(capacity)capacity75%,所以值为3
    • 3 + 1 <= 3,表达式不成立;
    • 超过了75%,所以走else进行扩容;
  • capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
    • 扩容策略是进行二倍扩容:capacity = 4 * 2 = 8
  • bucket_t *b = buckets();
    • 获取当前buckets容器;
  • mask_t m = capacity - 1;
    • m = 8 - 1 = 7,所以前面打印的时候_maybeMask = 7
  • reallocate(oldCapacity, capacity, true);
    • oldCapacity4capacity8
    • 进行空间的重新开辟,因为传入了true,会将旧的空间清空;
    • 可下面查看reallocate函数的解析。
  • 继续执行上面说过的插入操作,不再赘述。

reallocate 函数解析

reallocate()函数,和用到的相关函数

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

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

void cache_t::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_t::collectNolock(false);
}

void cache_t::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_t::collectNolock(false);
}
复制代码

第一次插入方法时,进入reallocate函数:

  • reallocate(oldCapacity, newCapacity, /* freeOld */false);
    • oldCapacity:旧的内存空间,第一次为0
    • newCapacity:新的内存空间,第一次为4
    • freeOld:是否回收旧的内存空间,第一次为false不回收,因为没得回收。
  • bucket_t *newBuckets = allocateBuckets(newCapacity);
    • Buckets赋值,开辟新的容器4个内存空间;
  • setBucketsAndMask(newBuckets, newCapacity - 1);
    • Bucketsmask赋值;
    • 注意:开辟了4个内存,但是给mask赋值是4 - 1
    • 可下面查看setBucketsAndMask函数的解析。

第一次扩容进入时,进入reallocate函数:

  • reallocate(oldCapacity, newCapacity, /* freeOld */false);
    • oldCapacity:旧的内存空间为4
    • newCapacity:新的内存空间为8
    • freeOld:是否回收旧的内存空间,第一次为false不回收;
  • bucket_t *newBuckets = allocateBuckets(newCapacity);
    • Buckets赋值,开辟新的容器8个内存空间;
  • setBucketsAndMask(newBuckets, newCapacity - 1);
    • Bucketsmask赋值;
    • 注意:开辟了8个内存,但是给mask赋值是8 - 1
    • 可下面查看setBucketsAndMask函数的解析。
  • if (freeOld)
    • 扩容时为true,第一次时为false
    • 这次扩容为true,所以走执行体代码。
  • collect_free(oldBuckets, oldCapacity);
    • 清空oldBucketsoldCapacity
    • 所以扩容后say1say2没有打印;

为什么不把之前的say1say2拿过来呢?

  • 之前已经开辟好的内存空间不能修改了,没办法直接追加;
  • 如果想把之前内存的数据都赋值到新的内存,消耗性能,缓存数据也会越积越多;
  • 方法越新越好,新方法更重要。

setBucketsAndMask 函数解析

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    #ifdef __arm__
    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();

    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);

    // ensure other threads see new buckets before new mask
    mega_barrier();

    _maybeMask.store(newMask, memory_order_relaxed);
    _occupied = 0;
#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;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}
复制代码

第一次插入方法时,进入setBucketsAndMask函数:

  • setBucketsAndMask(newBuckets, /* newMask */newCapacity - 1);
    • newBucketsstruct bucket_t *类型,是新的容器;
    • newMask:值为4 - 1 = 3是内存大小 - 1
  • _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
    • 开始存储,将newBuckets存储在_bucketsAndMaybeMask指针地址里面;
    • buckets的结构是一个哈希表,_bucketsAndMaybeMask中存储这buckets的首地址。
  • _maybeMask.store(newMask, memory_order_release);
    • newMask存储在_maybeMask中;
  • _occupied = 0;
    • 因为是新的容器,没有缓存,所以_occupied = 0

哈希函数 解析

哈希相关函数:

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);
}
复制代码

总结

bucket_t数据结构

  • sel方法编号,用来找到imp
  • imp函数指针地址,用来找到函数的实现

cache_t数据结构

  • _bucketsAndMaybeMask
    • 方法缓存列表buckets,以hash 表的形式存在;
    • buckets的首地址,存储在_bucketsAndMaybeMask中。
  • _maybeMask
    • 记录buckets的内存空间个数,实际为内存空间个数 - 1
  • _occupied
    • 记录buckets中缓存的方法个数。
  • _flags
    • 源码中暂时没注意到。

如果帮助到了您,点个?再走吧?。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享