OC底层原理6之类的cache与buckets

本章内容

  1. 类的cache结构,以及重要成员以及方法
  2. bucket_t的结构
  3. LLDB 进行验证cache缓存的方法
  4. 源码分析insert方法

类的第三个成员cache

类的本质以及结构已经清楚。类的本质就是objc_class *结构体指针。而成员从上到下依次是:

  1. Class isa,类的isa指向了其元类
  2. Class superClass,类的父类
  3. cache_t cache,类的缓存,缓存的是执行的方法
  4. class_data_bits_t bits,类的方法,协议,属性,成员变量等存储的地方。

cache的结构

cache顾名思义就是缓存,而且cache的存在也与objc_msgSend有着极大联系,我们可以想着OC的消息发送,转发的时候就能想到,它如果要想提高消息发送转发的效率就一定会有缓存这个东西,因为有缓存在快速发消息,转发的时候就会提高速率。所以我们研究类的cache也很有必要。我们为什么要引出下面的buckets其实从它结构体成员和方法可以看出来其重要程度,而buckets究竟是什么,请看下面的介绍

重要成员

  1. _bucketsAndMaybeMask:代表了buckets和maybeMask。它的作用其实如对象的isa一样就是为了用更少的内容却可以存储更多的信息。

  2. 联合体:可以看到有个 __LP64__ 代表了在MAC OS X 等系统运行,所以我们只用看结构体就行了

     1. 结构体{ 
             _maybeMask: 代表容器的大小。以每3/4的时候进行开辟,也就是
                         _occupied/_maybeMask为3/4进行开辟
             _occupied: 代表了缓存方法的数量
             }
     2. _originalPreoptCache
    复制代码

image.png

重要方法

从上之下进行说明,其中 insert 方法是最重要的,因为如果我们要进行读取一个缓存方法的时候,首先要这个方法在缓存里面,也就肯定要先去insert ,然后再去objc_msgSend。

  1. mask_t mask() const,获取maybeMask值,也就是容器的大小。

  2. void incrementOccupied(),提升occupied的大小,每次+1。

  3. void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask),将新的buckets存储到_bucketsAndMaybeMask里面,将新的mask(容器)存储到_maybeMask里面。

  4. void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld),创建新的buckets,其容器大小为newCapacity,并调用方法3 去存储,根据freeOld看是否需要释放旧的buckets。它其实内部调用了方法7

  5. void collect_free(bucket_t *oldBuckets, mask_t oldCapacity),暂时不知道

  6. static bucket_t *emptyBuckets(),清空buckets,这么说不准确,应该是将指针清空。

  7. static bucket_t *allocateBuckets(mask_t newCapacity),创建buckets

  8. static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true),如果满足条件调用方法6,将buckets清空,如果allocate为false的时候会直接返回nil,不满足的话要循环清除。说的并不特别准确,可以看源码

—–以上1-8都是私有方法,9开始是共有—-

  1. struct bucket_t *buckets() const, 获取buckets,LLDB调试会用

  2. mask_t occupied() const,获取 _occupied,缓存的方法数量

  3. void insert(SEL sel, IMP imp, id receiver),插入方法缓存,很重要。

bucket_t结构

我们根据cache就已经知道了buckets的重要性,但是buckets是什么呢?
bucket_t * 是一个结构体指针,它只是指向了一个内存中的地址,但是它代表什么其实我们是不确定的。例如void *它也仅仅是一个指针,指向内存地址。但是我们取出来的是一串16进制的数据而已。为什么我们平时用Class不用带 * 号去指向是因为Class本质就是objc_class *而已。而本文中的bucket_t *它其实代表的是一个哈希链表结构,它是无序的。

image.png

LLDB进行探究cache缓存的方法

在这里不贴源码,只需知道一个类Person有一个实例方法叫testMethod。然后我们只需要断点Person *p = [Person alloc]的下一行。 注意本次LLDB 是在源码工程调试。

如果说需要在无源码环境下进行调试也是可以的,我们可以将这些结构体给写在自己工程中,既然我们已经知道了它内存的分布情况就可以直接强制转换就行了,到时候也可以打印输出其缓存的方法

1. 先看没有调用方法的cache结构

image.png

2. 然后LLDB 调用方法 进行查看

然后发现_maybeMask为7,而_occupied为1,这就证明了其实我们调用方法过后会有变化。大家请看注意事项

注意:如果源码进行调用方法的话其实 _occupied为1,而_maybeMask为3。这是因为我们在用LLDB进行调用方法的时候系统自动会调用三个方法所以_maybeMask为7。

  1. 没有源码调用方法结果

image.png

  1. 源码调用方法结果

image.png

3. 查看cache究竟缓存了什么方法

可以查看到它其实在第5个平移位置缓存了方法。正是我在bucket_t中说道它是哈希链表结构是无序的。其实它的存储方法的方式是根据哈希函数来的,是有迹可寻的

image.png

insert方法的探究

为什么要研究这个方法?

  1. 从这个方法中我们能获得缓存方法的存储数量 _ocuupied
  2. 我们能知道方法的容器大小_maybeMask是在什么时候进行扩容的,怎么扩容的
  3. 当我们每次缓存方法后它如果在3/4临界点会将旧的方法移除,然后新的方法插入。(为什么要将旧的方法移除,是因为苹果认为新的方法进来后其实旧的方法不那么重要,而苹果遵从越新越好)
  4. 只有方法插入到缓存以后,objc_msgSend才会更快的查找

源码

里面的重点就是:容器buckets的开辟存储,用哈希函数计算方法的插入位置,

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    //runtime加个锁不用管
    runtimeLock.assertLocked();
    //不用看
    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }
    //不用看
    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }
    //宏定义不用看
#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif
    ASSERT(sel != 0 && cls()->isInitialized());

    // Use the cache as-is if until we exceed our expected fill ratio.
    //给缓存的方法数量进行赋值,第一次为0+1,后面的时候为x+1
    mask_t newOccupied = occupied() + 1;
    // 为 maybeMask, 即缓存方法锁开辟的空间数量,第一次为0
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    // 第一次进入的时候进该方法
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.第一次进来的时候capacity = 4
        if (!capacity) capacity = INIT_CACHE_SIZE;
        // 进行开辟容器的空间,数量,false:不用释放旧的缓存方法
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    //当缓存的方法数量少于开辟的容器数量的3/4的时候。
    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.
    }
    //如果架构为__arm64__ && __LP64__ 的时候才走
#if CACHE_ALLOW_FULL_UTILIZATION
    // 如果 capacity<=8 && newOccupied + (在一些架构中为0一些为1) <= capacity
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    //一般是当容器大小的3/4倍 < 缓存方法数量
    else {
        //如果 capacity为0的时候 赋值为4,至于为什么最终结果为3是reallocate方法
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        //容器最大数量为 1 << 16 = 0x1后面15个0, = 65536
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        // 进行开辟容器的空间,数量,true:释放旧的缓存方法
        reallocate(oldCapacity, capacity, true);
    }
    // 获取缓存方法的容器(哈希链表数组)
    bucket_t *b = buckets();
    // m 第一次为3
    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.
    
    do {
        //如果说需要插入的方法不在缓存中
        if (fastpath(b[i].sel() == 0)) {
            // occupied + 1
            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 循环条件:如果有哈希冲突就用 再哈希法 解决冲突
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
复制代码

补充reallocate方法

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    // 获取旧的容器
    bucket_t *oldBuckets = buckets();
    // 创建新的容器,容器的大小为 newCapacity
    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);
    
    //将容器的buckets与maybeMask存储也就是 _bucketsAndMaybeMask和_maybeMask
    // 如果是第一次进行申请,则容器大小为3
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    // 如果需要释放旧的容器的话进行释放
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享