OC底层原理07 - 类结构探索(2)

2020-09-29  本文已影响0人  卡布奇诺_95d2

类结构探索(1)中,对类结构中的isa进行了探索,接下来将对类结构中的其它成员进行探索。

cache_t cache

cache主要是用来缓存方法的,但如何缓存还需要我们去探索,首先来看一下cache_t这个结构体。

struct cache_t {
//表示运行的环境 模拟器 或者 macOS
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    // 是一个结构体指针类型,占8字节
    explicit_atomic<struct bucket_t *> _buckets; 
    //mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
    explicit_atomic<mask_t> _mask; 

//表示运行环境是 64位的真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //指针类型,占8字节
    explicit_atomic<uintptr_t> _maskAndBuckets; 
    //mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
    mask_t _mask_unused; 
    
#if __LP64__
    //uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    uint16_t _flags;  
#endif
    //uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    uint16_t _occupied; 
}

cache_t结构体在不同的架构中含有的属性个数不同,在真机中对mask和buckets存储进行了优化,将这两个属性存储到一个指针里面。

以下以macOS为例进行说明。

struct bucket_t {
private:
#if __arm64__ //真机
    //explicit_atomic 是加了原子性的保护
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else //非真机
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
    //方法等其他部分省略
}

我们通过一个示例来进行探索。

准备工作

@interface LGPerson : NSObject

@property(nonatomic, strong)NSString* name;
@property(nonatomic, strong)NSString* nickName;

- (void)say111;

- (void)say222;

- (void)say333;

- (void)say444;

- (void)say555;

@end

@implementation LGPerson
- (void)say111{
    NSLog(@"%s", __func__);
}

- (void)say222{
    NSLog(@"%s", __func__);
}

- (void)say333{
    NSLog(@"%s", __func__);
}

- (void)say444{
    NSLog(@"%s", __func__);
}

- (void)say555{
    NSLog(@"%s", __func__);
}

@end3
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //0x00007ffffffffff8ULL
        LGPerson* person = [LGPerson alloc];
        Class pClass = [LGPerson class];
        [person say111];
        [person say222];
        [person say333];
        [person say444];
        [person say555];
    }
    return 0;
}

开始探索

  1. 获取类的首地址
(lldb) p/x pClass
(Class) $0 = 0x0000000100008320 LGPerson
  1. 由于类结构体中的前两个成员为isasuperclass,各占8个字节,因此,将首地址偏移16个字节,即为cache起始地址
(lldb) p/x (cache_t*)0x0000000100008330
(cache_t *) $1 = 0x0000000100008330
  1. 读取cache中的内容
(lldb) p *$1
(cache_t) $2 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = {
      Value = 0x0000000100346460
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = {
      Value = 0
    }
  }
  _flags = 32804
  _occupied = 0
}
  1. 获取buckets中的selimp
(lldb) p $2.buckets()[0].sel()
(SEL) $3 = <no value available>

由此时可以看出,当未调用对象方法时,cache中没有缓存

  1. 调用一次对象方法后,再读取一次buckets中的selimp
(lldb) p *$1
(cache_t) $4 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = {
      Value = 0x000000010070ea30
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = {
      Value = 3
    }
  }
  _flags = 32804
  _occupied = 1
}

(lldb) p $2.buckets()[0].sel()
(SEL) $5 = "say111"

此时可以发现,当调用了一次对象方法后,cache中缓存一次方法
那再调用一次对象方法呢,是不是又会缓存一次?为了验证这个想法,让应用再调用一次对象方法后,再查看一下当前cache中的内容。

(lldb) p *$1
(cache_t) $7 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = {
      Value = 0x000000010070ea30
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = {
      Value = 3
    }
  }
  _flags = 32804
  _occupied = 2
}

(lldb) p $7.buckets()[0].sel()
(SEL) $8 = "say111"
(lldb) p $7.buckets()[1].sel()
(SEL) $9 = "say222"

总结:

那调用对象方法时,是如何将方法存入cache中?

由于每一次调用,会对_occupied值进行加1,那就先从这个值着手。

void cache_t::incrementOccupied() 
{
    _occupied++;
}
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);
    }

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

该方法就是cache插入,即向cache中插入sel、imp
接下来我们来分析一下这个方法。

if (slowpath(isConstantEmptyCache())) { //小概率发生的 即当 occupied() = 0时,即创建缓存,创建属于小概率事件
    // Cache is read-only. Replace it.
    if (!capacity) capacity = INIT_CACHE_SIZE; //初始化时,capacity = 4(1<<2 -- 100)
    reallocate(oldCapacity, capacity, /* freeOld */false); //开辟空间
    //到目前为止,if的流程的操作都是初始化创建
}

如果缓存占用量小于等于3/4,则不作任何处理

else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { 
    // Cache is less than 3/4 full. Use it as-is.
}

如果缓存占用量超过3/4,则需要进行两倍扩容以及重新开辟空间

else {//如果超出了3/4,则需要扩容(两倍扩容)
    //扩容算法: 有cap时,扩容两倍,没有cap就初始化为4
    capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍 2*4 = 8
    if (capacity > MAX_CACHE_SIZE) {
        capacity = MAX_CACHE_SIZE;
    }
    // 走到这里表示 曾经有,但是已经满了,需要重新梳理
    reallocate(oldCapacity, capacity, true);
    // 内存 扩容完毕
}

到此,cache_t的原理基本分析完成了。

接下来有几个问题为重点面试问题:

  1. bucket数据为什么会有丢失的情况?
    答:原因是在扩容时,是将原有的内存全部清除了,再重新申请了内存导致的。

  2. 为什么随着方法调用的增多,其打印的occupied 和 mask会变化?
    答:因为在cache初始化时,分配的空间是4个,随着方法调用的增多,当存储的sel-imp个数,即newOccupied + CACHE_END_MARKER的和超过总容量的3/4,例如有4个时,当occupied等于2时,就需要对cache的内存进行两倍扩容。

  3. say333、say444的打印顺序为什么是say444先打印,say333后打印,且还是挨着的,即顺序有问题?
    答:因为sel-imp的存储是通过哈希算法计算下标的,其计算的下标有可能已经存储了sel,所以又需要通过哈希冲突算法重新计算哈希下标,所以导致下标随机的,并不是固定的。

上一篇下一篇

猜你喜欢

热点阅读