前言
这篇文章将对类结构中cache_t的分析。
cache_t的结构
cache_t源码。
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED//macOS、模拟器
// explicit_atomic 显示原子性,目的是为了能够 保证 增删改查时 线程的安全性
//等价于 struct bucket_t * _buckets;
//_buckets 中放的是 sel imp
//_buckets的读取 有提供相应名称的方法 buckets()
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //64位真机
explicit_atomic<uintptr_t> _maskAndBuckets;//写在一起的目的是为了优化
mask_t _mask_unused;
//以下都是掩码,即面具 -- 类似于isa的掩码,即位域
// 此处省略....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非64位 真机
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
//以下都是掩码,即面具 -- 类似于isa的掩码,即位域
// 此处省略....
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
//方法省略.....
}
- 从源码中可以看出,在macOS、64位真机和非64位真机环境下有不同的处理。
CACHE_MASK_STORAGE_OUTLINED
macOS、模拟器CACHE_MASK_STORAGE_HIGH_16
64位真机CACHE_MASK_STORAGE_LOW_4
非64位 真机explicit_atomic
显示原子性,为线程的安全。- 其中在真机环境下,mask和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
// Compute the ptrauth signing modifier from &_imp, newSel, and cls.
uintptr_t modifierForSEL(SEL newSel, Class cls) const {
return (uintptr_t)&_imp ^ (uintptr_t)newSel ^ (uintptr_t)cls;
}
bucket_t
分为两个版本,真机
和非真机
,不同的区别在于sel
和imp
的顺序不同。- 从上面的源码中,也可以清晰看出,
cache_t
缓存了imp
和sel
,具体存储在bucket_t
中。
获取_buckets
- 既然我们知道
imp
和sel
存储在_buckets
中,那么如何获取呢? - 定义一个Person类继承NSobject。
@interface Person : NSObject{
NSString *hobby;
}
@property (nonatomic,copy) NSString *name;
- (void)eat;
- (void)say;
+ (void)run;
@end
@implementation Person
- (void)eat{
NSLog(@"eat something");
}
-(void)say{
NSLog(@"say hi");
}
+ (void)run{
NSLog(@"run run");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
Class pClass = object_getClass(person);
[person eat];
[person say];
[person say];
}
return 0;
}
- 在
[person eat]
之前打断点,通过lldb调试工具,查看person的类的内存结构,lldb执行p/x pClass
获取首地址。
(lldb) p/x pClass
(Class) $0 = 0x00000001000033a8 Person
- 前面文章中,我们知道了类的内存布局,是以
isa
、superclass
、cache_t
、class_data_bits_t
顺序排布的,其中isa
与superclass
分别占用8
个字节,所以cache_t
的位置应该是从类的首地址偏移16字节
的位置,所以我们取0x00000001000033a8
偏移16
字节,即0x00000001000033b8
的地址。 - 执行
p (cache_t *)0x00000001000033b8
获取cache_t
的首地址。
(lldb) p (cache_t *)0x00000001000033b8
(cache_t *) $1 = 0x00000001000033b8
- 执行
p *$1
,打印cache
(lldb) p *$1
(cache_t) $2 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x000000010032f410 {
_sel = {
std::__1::atomic<objc_selector *> = (null)
}
_imp = {
std::__1::atomic<unsigned long> = 0
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 0
}
_flags = 32804
_occupied = 0
}
(lldb)
- 此时没有调用方法,
_sel = null
,_imp = 0
,_occupied = 0
。 - 我们打个断点在
[person say]
处,上面[person eat]
的断点执行下一步step over
。 - 继续执行
p *$1
。
(lldb) p *$1
(cache_t) $3 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x0000000100694ab0 {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 10424
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 3
}
_flags = 32804
_occupied = 1
}
- 此时的,
_sel
和_imp
都有值,_occupied = 1
。但要怎么获取_buckets
,于是看了看cache_t
的源码,终于有了新的发现。 - 于是很愉快的执行
buckets()
去获取,p $3.buckets()
。
(lldb) p $3.buckets()
(bucket_t *) $4 = 0x0000000100694ab0
- 执行
p *$4
打印_buckets
信息。
(lldb) p *$4
(bucket_t) $5 = {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 10424
}
}
- 那么如何才能获得
_sel
和_imp
呢,于是才看_buckets
源码,在源码中看到了,获取_sel
和_imp
的方法。 - 执行
p $4.sel()
,获取sel
(lldb) p $5.sel()
(SEL) $6 = "eat"
- 执行
p $5.imp(pClass)
,获取imp
(lldb) p $5.imp(pClass)
(IMP) $7 = 0x0000000100001b10 (KCObjc`-[Person eat])
获取_buckets小结
- 1、
cache
的获取,需要通过pClass
的首地址平移16
字节,即首地址+0x10
获取cache的地址
。 - 2、
cache_t
结构体中提供了buckets()
获取_buckets
。 - 3、
_buckets
结构体中,通过sel()
和imp(pClass)
分别获取sel
和imp
。 - 4、没有调用方法时,cache是没有缓存的,调用方法后,cache中就有了一个缓存,即调用方法后就会缓存在
cache
中。
cache_t的结构图
cache怎么存?
- 在
cache_t
源码中,找到了引起变化的函数,incrementOccupied()函数。
void cache_t::incrementOccupied()
{
_occupied++; //occupied自增
}
- 全局搜索
incrementOccupied()
函数,发现只在cache_t的insert方法有调用 - 全局搜索
insert(
函数,发现cache_fill
才符合调用 - 全局搜索
cache_fill
,消息发送之后,会获取imp,接着才调用cache_fill
方法写入缓存,即objc_msgSend->cache_getImp->cache_fill
insert源码分析
- insert()中,其源码实现如下
cache->insert
函数大致做了3
件事
1、初始化缓存空间
2、判断是否需要扩容,如果需要,以原始空间的2倍扩容,重新分配空间,释放已有缓存信息
3、根据散列表中是否已有该方法的缓存情况插入缓存
1、初始化缓存空间
- 如果
occupied()
为0, 并且buckets
中无缓存内容 , 则开辟4
个存储空间大小 为默认初始值
。(4 来自于if (!capacity) capacity = INIT_CACHE_SIZE
)
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
// 1 << 2 = 4
- 调用
reallocate(oldCapacity, capacity, /* freeOld */false);
分配空间。
ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
// 开辟空间
bucket_t *newBuckets = allocateBuckets(newCapacity);
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
// 释放旧的缓存信息
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
}
}
- 调用
allocateBuckets
bucket_t *allocateBuckets(mask_t newCapacity)
{
// Allocate one extra bucket to mark the end of the list.
// This can't overflow mask_t because newCapacity is a power of 2.
bucket_t *newBuckets = (bucket_t *)
calloc(cache_t::bytesForCapacity(newCapacity), 1);
bucket_t *end = cache_t::endMarker(newBuckets, newCapacity);
#if __arm__
// End marker's sel is 1 and imp points BEFORE the first bucket.
// This saves an instruction in objc_msgSend.
end->set<NotAtomic, Raw>((SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
// End marker's sel is 1 and imp points to the first bucket.
end->set<NotAtomic, Raw>((SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
reallocate
函数中 通过 allocateBuckets
函数的 calloc
向系统申请 newCapacity
大小的空间;
并且通过 setBucketsAndMask
设置 buckets
和 mask
,其中 mask 更新为 新申请的总空间大小 - 1 (capacity - 1);
2、判断空间是否足够
- 如果空间不足, 扩容到原空间大小的2倍值,并重新分配空间大小 并释放已存储的缓存,插入新缓存。
// arm64下 如果 newOccupied <= 容量的4分之3,存储空间还足够,不需额外处理
else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
// Cache is less than 3/4 full. Use it as-is.
}
// 如果超过 4分之3
else {
// 扩容为原空间的 2倍大小
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE; // 最大不能 超出 1<< 16
}
reallocate(oldCapacity, capacity, true); // 重新分配空间 存储新的数据,抹除已有缓存
}
3、插入缓存
/**
3注: 以下为插入缓存的过程
遍历 buckets()内容,如果在缓存中找到了 传入的方法,直接退出
如果在缓存中没有找到 传入的方法 将_occupied ++;,并且将方法存入缓存
如果遇到hash冲突, cache_t查找下一个 直到回到begin 全部查找结束
*/
// 获取散列表
bucket_t *b = buckets();
// 获取散列表大小 - 1
mask_t m = capacity - 1;
// 通过cache_hash函数【begin = sel & m】计算出key值 k 对应的 index值
// begin,用来记录查询起始索引
mask_t begin = cache_hash(sel, m);
// begin 赋值给 i,用于切换索引
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) { // 如果没有找到缓存的方法
incrementOccupied(); // _occupied ++;
b[i].set<Atomic, Encoded>(sel, imp, cls); // 缓存实例方法
return;
}
if (b[i].sel() == sel) { // 如果找到需要缓存的方法,什么都不做,并退出循环
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
// 当出现hash碰撞 cache_t查找下一个 直到回到begin 全部查找结束
cache_t::bad_cache(receiver, (SEL)sel, cls);
begin 作为 散列表 的初始查询下标,是经过 sel & mask 计算而来的
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
do ... while 循环的条件是 (i = cache_next(i, m) != begin ,判断不等于初始下标值 begin 是为了将散列表中的数据全部遍历结束,而cache_next( ) 是为了解决哈希冲突而进行的二次哈希。
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}