objc_class|objc_class 中 cache 原理分析

OC底层原理学习
回顾类的结构分析中中提到的objc_class结构
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags ... }

cache_t 结构分析 【objc_class|objc_class 中 cache 原理分析】这一章探索一下cache,顾名思义是一个缓存,存储的是什么东西?打开cache_t的源码
struct cache_t { #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED//macOS、模拟器 explicit_atomic _buckets; explicit_atomic _mask; #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16//真机64位 explicit_atomic _maskAndBuckets; mask_t _mask_unused; // How much the mask is shifted by. ... #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4//真机非64位 // _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 _maskAndBuckets; mask_t _mask_unused; ... #else #error Unknown cache mask storage type. #endif#if __LP64__ uint16_t _flags; #endif uint16_t _occupied; ...

  • catch的结构中分为3个架构分别是mac系统、64位真机、非64位真机
  • explicit_atomic 显示原子性,目的是为了能够保证增删改查时线程的安全性等价于struct bucket_t * _buckets
  • 在模拟器和真机属性上是有区别的,模拟器是有_buckets_mask,而真机只有一个_maskAndBuckets,合成一个的目的是为了节省内存,读取方便
接下来再看一下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 _imp; explicit_atomic _sel; #else explicit_atomic _sel; explicit_atomic _imp; #endif ... }

  • bucket_t的结构中分为2个两个架构,一个真机一个非真机
  • 有两个属性_imp_sel
所以cache中缓存的是sel-imp,这不是就是方法么!猜测,我们每调用一个方法,都会被缓存下来,通过lldb命令,打印一下
step1. 初始化一个Person类,在调用方法之前添加断点,根据类的结构分析中得知,对象方法存储在类中,获取类的地址信息
(lldb) p/x person.class (Class) $0 = 0x00000001000022d0 Person

step2.地址偏移16位,获取cache_t,并打印
(lldb) p/x 0x00000001000022e0 (long) $1 = 0x00000001000022e0 (lldb) p (cache_t *)0x00000001000022e0 (cache_t *) $2 = 0x00000001000022e0 (lldb) p *$2 (cache_t) $3 = { _buckets = { std::__1::atomic = 0x000000010074bc30 { _sel = { std::__1::atomic = (null) } _imp = { std::__1::atomic = 0 } } } _mask = { std::__1::atomic = 3 } _flags = 32804 _occupied = 2 }

这时候我们看到buckets还是null
step3. 手动触发一次对象方法,再打印出cache_t
(lldb) po [person objcMethod] (lldb) p *$2 (cache_t) $4 = { _buckets = { std::__1::atomic = 0x000000010074bc70 { _sel = { std::__1::atomic = "" } _imp = { std::__1::atomic = 12064 } } } _mask = { std::__1::atomic = 7 } _flags = 32804 _occupied = 1 }

step4. 可以看到已经不是null了 ,通过cache_t提供的buckets()方法,获取bucket_t对象
(lldb) p $4.buckets() (bucket_t *) $5 = 0x000000010074bc70 (lldb) p *$5 (bucket_t) $7 = { _sel = { std::__1::atomic = "" } _imp = { std::__1::atomic = 12064 } }

step5. 通过bucket_t提供的sel()imp(Class cls)两个方法,获取sel-imp
(lldb) p $7.sel() (SEL) $8 = "objcMethod" (lldb) p $7.imp(person.class) (IMP) $10 = 0x0000000100000df0 (Example`-[Person objcMethod])

在没有执行方法调用时,cache是没有缓存的,执行了一次方法调用,cache中就有了一个缓存,即调用一次方法就会缓存一次方法,缓存的目的无非也就是再次调用的时候,使用缓存,猜测查找方法的时候可能会耗时,不耗时没必要使用缓存,这个查先以后查询,我们先研究一下,他是什么时候如何插入到缓存中的
cache_t 插入 插入的方法猜测99.99%是cache_t提供的,在cache_t提供的方法中,
发现了void insert(Class cls, SEL sel, IMP imp, id receiver); ,来到它的实现,添加一个断点,看一下堆栈

objc_class|objc_class 中 cache 原理分析
文章图片

触发它的条件是,系统底层发出了一个未缓存的消息,emmm,符合我们的猜想,消息的发送机制以后再研究,先研究插入流程
objc_class|objc_class 中 cache 原理分析
文章图片

将源码分为三部分看
  1. 计算缓存数量
mask_t newOccupied = occupied() + 1; // 新的缓存数量 = 已缓存的数量 + 1

  1. 开辟空间
  • 第一次进来开辟4个
if (slowpath(isConstantEmptyCache())) { // 编译器优化:小概率发生没有缓存的时候 // Cache is read-only. Replace it. if (!capacity) capacity = INIT_CACHE_SIZE; //默认值初始化4 reallocate(oldCapacity, capacity, /* freeOld */false); //开辟空间 }

bool cache_t::isConstantEmptyCache() { return occupied() == 0&& buckets() == emptyBucketsForCapacity(capacity(), false); }

enum { INIT_CACHE_SIZE_LOG2 = 2, INIT_CACHE_SIZE= (1 << INIT_CACHE_SIZE_LOG2), MAX_CACHE_SIZE_LOG2= 16, MAX_CACHE_SIZE= (1 << MAX_CACHE_SIZE_LOG2), };

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 thisASSERT(newCapacity > 0); ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1); setBucketsAndMask(newBuckets, newCapacity - 1); if (freeOld) { cache_collect_free(oldBuckets, oldCapacity); } }

  • 小于等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 ,什么也不做 }

  • 超过3/4进行扩容,并重新梳理缓存
//当前容量不为空时,扩容2倍,如果为空:初始化 = 4 capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; if (capacity > MAX_CACHE_SIZE) { capacity = MAX_CACHE_SIZE; //最大不能 超出 1<< 16 } //释放旧的,缓存新的 reallocate(oldCapacity, capacity, true);

  1. 进行imp和sel赋值
//获取散列表 bucket_t *b = buckets(); // 获取散列表大小 - 1 mask_t m = capacity - 1; // 通过cache_hash方法获取 对应的 index值 // begin,用来记录查询起始索引 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(); //_occupied ++; b[i].set(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)); //当出现hash碰撞 cache_t查找下一个 直到回到begin 全部查找结束cache_t::bad_cache(receiver, (SEL)sel, cls);

关于Cache_t原理分析图 objc_class|objc_class 中 cache 原理分析
文章图片

    推荐阅读