1.前言
在003-类的底层探索中,我们研究了类里的bits里的内容。
superclass很明显是一个8字节的指向父类的指针。
那么cache里面存储的是什么呢?
今天,就让我们来研究一下cache_t。
首先计算偏移量=isa的大小+superclass的大小=8+8=16字节=0x10
2.cache的基本数据结构
查看cache_t源码
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
//......
bool isConstantEmptyCache() const;
bool canBeFreed() const;
mask_t mask() const;
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
static bucket_t *emptyBuckets();
static bucket_t *allocateBuckets(mask_t newCapacity);
static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));
public:
//....
unsigned capacity() const;
struct bucket_t *buckets() const;
Class cls() const;
mask_t occupied() const;
void initializeToEmpty();
void insert(SEL sel, IMP imp, id receiver);
void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
void destroy();
void eraseNolock(const char *func);
static void init();
static void collectNolock(bool collectALot);
static size_t bytesForCapacity(uint32_t cap);
//....
};
_bucketsAndMaybeMask 变量与isa_t的bits类似,是一个指针类型存放地址。
联合体里有一个结构体和一个 结构体指针_originalPreoptCache。
结构体里有2/3个成员变量
- explicit_atomic<mask_t> _maybeMask,当前缓存区count,第一次开辟是3。
- 如果是LP64(指的是Unix系统/类Unix系统 macOS也是)则有uint16_t _flags;
- uint16_t _occupied:表示cache已存储的buckets数量,默认为0。
由于是联合体,结构体和 _originalPreoptCache是互斥的。
查看他的插入函数 寻找数据存储在哪里
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
//...........
bucket_t *b = buckets();
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);
}
可以发现它在一个bucket_t *b=buckets()获取到当前的bucket,然后进行插入。
查看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<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
//...
public:
static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
//SEL
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
//...
template <Atomicity, IMPEncoding>
void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};
里面存了imp和sel。imp和sel的关系如图所示:
可以发现是这样的一个关系
3.LLDB调试验证
获取到TWPerson类的指针地址,加上偏移量0x10
断点到这 TWPerson *p = [[TWPerson alloc] init];
再打印发现Value=3,occupied=2
获取其bucket
调用对象方法sayhi1,观察其变化,发现value=7,occupied=3
打印其buckets,发现buckets缓存了sayhi1。
这里简单的说一下为啥buckets的存储插入是乱序的:
实际上这里的buckets使用的哈希表存储的方式,结合了数组和链表的优势,方便插入与查找。
并使用拉链法解决哈希冲突。
4.脱离源码环境
为了便于之后的探索,我们可以脱离源码环境,自己写出源码相似的结构,然后将程序内的cache_t强制转换成我们写的结构。结构体代码如下:
#import <Foundation/Foundation.h>
#import "TWTeacher.h"
#import <objc/runtime.h>
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct tw_cache_t {
//uintptr_t _bucketsAndMaybeMask;//8
struct tw_bucket_t *_buckets;
mask_t _maybeMask;//4
uint16_t _flags;//2
uint16_t _occupied;//2
};
struct tw_class_data_bits_t {
uintptr_t bits;
};
struct tw_bucket_t {
SEL _sel;
IMP _imp;
};
struct tw_objc_class {
// 继承objc_object Class ISA;
Class ISA;
Class superclass;
struct tw_cache_t cache; // formerly cache pointer and vtable
struct tw_class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
main函数如下
/*
解决问赋
1.源码无法调试
2.无需LLDB调试
3.小规模取样 变得简单清晰
a:1-3->1-7
b:(null)->0x0 方法去哪
c:2-7 +say4+没有类方法
d:拿到了父类方法
*/
int main(int argc, const char * argv[]) {
@autoreleasepool {
TWPerson *p=[[TWPerson alloc]init];
Class pClass=p.class;
[p sayhi1];
[p sayhi2];
[p sayhi3];
[p sayhi4];
[p sayhi5];
[p sayhi6];
struct tw_objc_class *twclass=(__bridge struct tw_objc_class *)(pClass);
for(mask_t i=0;i<twclass->cache._occupied;i++){
struct tw_bucket_t bucket=twclass->cache._buckets[i];
NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
}
NSLog(@"%@",twclass);
NSLog(@"%hu-%u",twclass->cache._occupied,twclass->cache._maybeMask);
}
return 0;
}
5.提出问题
- 发现这里面消失了sayhi1。很明显是缓存机制,将sayhi1踢出了缓存,可明明是有7个位置,为什么没有存储sayhi1呢?
- 我们之前LLDB调试也发现_maybeMask一开始从0,变成了3,之后又变成了7,这具体是怎么回事呢?
接下来我们进行cache_t的源码探索。
6.源码探索
要想知道它为什么这么变化,我们得好好把插入sel和imp的insert函数给看懂了,一开始我给的省略了许多关键代码,接下来,我们一点点分析。
6.1 insert第一次插入情况
首先分析insert函数中的第一次插入情况,假设是我们是X86框架。
这里第一次,occupied()默认初始化为0,newOccupied=1,capacity=4;
查看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);
}
}
里面有个setBucketsAndMask函数 点进去发现不同架构有不同的操作,我们摘出x86的来分析。
#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;
可以看出,_bucketsAndMaybeMask中存储的内容是newMask和newBuckets,newBuckets是函数的第一个参数,是新开辟的内存空间地址,newMask是函数的第二个参数,即newCapacity-1,即是开辟容量-1,此时为3。
代码跑到这里,第一次cache_t插入时的初始化就完成了。
我们继续看插入,已注释。
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);
}
这里也解释了为什么buckets的下标是乱序的,以及如何解决的哈希冲突。
接着我们看后续的插入,这里分3种情况(加一个初始化,insert里面就一共有1+3=4种情况)
6.2 insert后续插入情况分支1
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
newOccupied是已存储的buckets数量+1 再+1如果小于容量的3/4就无事发生,之后正常插入。
6.3 insert后续插入情况分支2
判断是否cache允许全部占据,是上一个3/4扩容的另一种情况,插满才扩容,实际上不会走这里。
6.4 insert后续插入情况分支3 (扩容)
此时扩容,扩容方式为两倍扩容,假设是第一次扩容,就是capacity=4*2=8。
此外还设置了cache的最大缓存容量:1<<16=2^16=65536。
MAX_CACHE_SIZE_LOG2 = 16,
MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2),
如果到了最大缓存容量,就停止扩容。
再使用reallocate函数,在6.1已经分析过了,实际上释放了之前创建的buckets,这就解释了为什么之前的sayhi1被抛弃了,实际上是因为重新申请了一个新的更大内存的buckets。
7.结尾
探索告一段落,如果任何问题,欢迎留言或联系我!谢谢观看。
8.参考文献
1.cache底层分析 https://www.jianshu.com/p/ced2e2ef7468