「OC」源码学习——cache_t的原理探究
「OC」源码学习——cache_t的原理探究
前言
上一次我们讲到了objc_class
的isa和bits之中的内容,接下来就是cache_t
之中的内容。
cache_t探究
前面我们提到在类对象之中isa
和superclass
各占8个字节,所以我们可以直接通过内存平移获取相关内容,于是我们打开lldb继续调试
好吧,看起来好像一点有用的看不到,没关系,我们接着查找到cache_t的定义
可以看到在当前架构下,有用的属性为 _bucketsAndMaybeMask
,_unused
,_flags
,_occupied
,_originalPreoptCache
,但由于_originalPreoptCache
和_unused
,_flags
,_occupied
组成的结构体组成了联合体,由于联合体的互斥特性,我们可以先不看_originalPreoptCache
,_unused
是占位符可以实现八位对齐
通过源码的学习,我们可以知道这时候其实可以通过cache提供给的buckets()
方法去获取_buckets
,就可以获取sel-imp
了,这两个的获取在bucket_t
结构体中同样提供了相应的获取方法sel()
以及 imp(pClass)
,我们可以通过内存偏移的方法去获取第二个
总结出cache_t的结构如上图
insert函数
在运行完-[GGObject speak]
方法之后,_occupied
进行了自增,而调用-[GGObject sayHello]
之后在查看发现_occupied
又变成了1,这是为什么呢?
我们来看看cache_t之中关于insert的函数,就是将函数缓存插入bucket_t
之中的实现
void cache_t::insert(SEL sel, IMP imp, id receiver)
{lockdebug::assert_locked(&runtimeLock);// Never cache before +initialize is doneif (slowpath(!cls()->isInitialized())) {return;}if (isConstantOptimizedCache()) {_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",cls()->nameForLogging());}#if DEBUG_TASK_THREADSreturn _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCKmutex_locker_t lock(cacheUpdateLock);
#endifASSERT(sel != 0 && cls()->isInitialized());// 缓存的容量管理mask_t newOccupied = occupied() + 1;unsigned oldCapacity = capacity(), capacity = oldCapacity;if (slowpath(isConstantEmptyCache())) {if (!capacity) capacity = INIT_CACHE_SIZE;//默认值为4reallocate(oldCapacity, capacity, /* freeOld */false);}else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {// 缓存没超过3/4或者7/8}
#if CACHE_ALLOW_FULL_UTILIZATIONelse if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {// 对于小容量缓存(<=8),允许完全利用存储空间}
#endifelse {capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;//两倍扩容if (capacity > MAX_CACHE_SIZE) {capacity = MAX_CACHE_SIZE;}reallocate(oldCapacity, capacity, true);//重新申请空间}bucket_t *b = buckets();//可以吧bucket_t理解为哈希表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);
#endif // !DEBUG_TASK_THREADS
}static inline mask_t cache_hash(SEL sel, mask_t mask)
{return (mask_t)(uintptr_t)sel & mask;
}//哈希算法static inline mask_t cache_next(mask_t i, mask_t mask) {return (i+1) & mask;
}//解决哈希冲突的方法
我们可以发现这个insert函数一共可以分为两个部分,一个是扩容判断,一个是插入缓存,在存储的时候使用哈希表,就是说明这些方法存放的地址并不一定连续。
我们可以看到_occupied
代表的其实就是bucket
进行内部存储的函数的个数,对于这个_occupied
有几个点是值得注意的
alloc
申请空间时,此时的对象已经创建
,如果再调用init
方法,occupied
也会+1
- 当
有属性赋值
时,会隐式调用set
方法,occupied
也会增加,即有几个属性赋值,occupied就会在原有的基础上加几个
- 当新的方法被调用时,
occupied
也会增加,即有几次调用,occupied就会在原有的基础上加几个
接着我们来理解的就是这个扩容操作了
- 首先如果缓存为空的话,就给
buckets
分配初始化的长度(x86_64为4,arm为2)并且创建一个buckets
; - 在arm64框架下,缓存的大小 <= buckets长度的7/8,并且buckets长度<=8没装满8,不扩容,在x86_64下,缓存的大小 <= buckets长度的3/4 ,不扩容。
- 扩容逻辑:对当前容量的
2倍扩容
,并且如果扩容后容量大小 >MAX_CACHE_SIZE
,则设置为MAX_CACHE_SIZE
;计算出扩容大小后,以这个容量去创建新的buckets,和释放旧的buckets
。
这就是为什么当我们调用类之中-[GGObject sayHello]
方法之后,反而_occupied
变回了1,这就是因为旧值的buckets
已经被完全释放,重新开辟了一个内存。
以下是开辟bucket
内存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);}
}
_bucketsAndMaybeMask
在 ARM64 架构 下,_bucketsAndMaybeMask
被设计为 复合字段:
- 高位存储 mask(16 位):
capacity - 1
。 - 低位存储 buckets 地址(48 位):指向
bucket_t
数组的指针。(因为ARM64 架构下虚拟地址为48位)
mask_t cache_t::mask() const
{uintptr_t maskAndBuckets = _bucketsAndMaybeMask.load(memory_order_relaxed);return maskAndBuckets >> maskShift;//maskShift大小为48位
}
struct bucket_t *cache_t::buckets() const
{uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);return (bucket_t *)(addr & bucketsMask);
}
程序可以通过以上形式分别获取mask和bucket。
参考文章
iOS-底层原理 11:objc_class 中 cache 原理分析
iOS底层-cache_t原理分析
Objective-C 类的底层探索