1、Cache_t结构
在objc_class
中除了isa
、superclass
、bits
三个重要的属性外还有一个重要的属性并未进行分析就是cache_t cache
,看源码对这个属性的注释:
cache_t cache; // formerly cache pointer and vtable
cache
可谓是在计算机软硬件很普遍的存在,从物理的到虚拟的都存在这个概念,从计算机三级缓存到NSURLCache
请求缓存,都是类似设计,所以这里的这个cache
显然就是为了优化方法调用效率而使用的。
可以大致知道内部结构是大致存的是指针
和表结构
。下面对源码结构进行解析:
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
// How much the mask is shifted by.
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
// Ensure we have enough bits for the buckets pointer.
// static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
// _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<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
static constexpr uintptr_t maskBits = 4;
static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
public:
static bucket_t *emptyBuckets();
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
unsigned capacity();
bool isConstantEmptyCache();
bool canBeFreed();
#if __LP64__
bool getBit(uint16_t flags) const {
return _flags & flags;
}
void setBit(uint16_t set) {
__c11_atomic_fetch_or((_Atomic(uint16_t) *)&_flags, set, __ATOMIC_RELAXED);
}
void clearBit(uint16_t clear) {
__c11_atomic_fetch_and((_Atomic(uint16_t) *)&_flags, ~clear, __ATOMIC_RELAXED);
}
#endif
#if FAST_CACHE_ALLOC_MASK
bool hasFastInstanceSize(size_t extra) const
{
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
}
return _flags & FAST_CACHE_ALLOC_MASK;
}
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;
uint16_t sizeBits;
// Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16
// to yield the proper 16byte aligned allocation size with a single mask
sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
sizeBits &= FAST_CACHE_ALLOC_MASK;
if (newSize <= sizeBits) {
newBits |= sizeBits;
}
_flags = newBits;
}
#else
bool hasFastInstanceSize(size_t extra) const {
return false;
}
size_t fastInstanceSize(size_t extra) const {
abort();
}
void setFastInstanceSize(size_t extra) {
// nothing
}
#endif
static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
void insert(Class cls, SEL sel, IMP imp, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn, cold));
};
结合一个类的代码:
@interface Person : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,strong) NSString *nickname;
@property (nonatomic,assign) float height;
@property (nonatomic,strong) NSString *name;
-(void)laugh;
-(void)cry;
-(void)run;
-(void)jump;
-(void)doNothing;
@end
@implementation Person
-(void)laugh
{
NSLog(@"LMAO");
}
-(void)cry
{
NSLog(@"cry me a river");
}
-(void)run
{
NSLog(@"run! Forrest run!");
}
-(void)jump
{
NSLog(@"you jump,I jump!");
}
-(void)doNothing
{
NSLog(@"Today,I dont wanna do anything~");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
//main方法中
Person *person = [Person alloc];
person.age = 10;
person.nickname = @"pp";
person.height = 180.0;
person.name = @"ppext";
[person laugh];
[person cry];
[person run];
[person jump];
[person doNothing];
[person run];
[person laugh];
[person doNothing];
}
return 0;
}
@end
将断点断在倒数第二个方法,然后进行lldb
调试,结合之前对bits
的调试方法,可以减少16字节偏移,将指针指向cache
:

cache_t
的在基本属性i386
、x86_64
架构下的属性是:buckets
、mask
、flags
、occupied
,和之前对cache_t
所占内存大小的计算是一致的。其中后三个结构简单,但是不知道意思,第一个可以通过lldb
配合cache_t
和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;
}
// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(IMP newImp, SEL newSel, Class cls) const {
if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
return (uintptr_t)
ptrauth_auth_and_resign(newImp,
ptrauth_key_function_pointer, 0,
ptrauth_key_process_dependent_code,
modifierForSEL(newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
}
public:
inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); }
inline IMP rawImp(objc_class *cls) const {
uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
return (IMP)imp;
}
inline IMP imp(Class cls) const {
uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order::memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
template <Atomicity, IMPEncoding>
void set(SEL newSel, IMP newImp, Class cls);
};
不管是哪种架构bucket_t
中存的都有SEL
和IMP
,这个基本上就是方法的主要信息了。开始对里面信息打印:

buckets
绝对不只有一个,那就进行地址偏移打印一下:
SEL
中的方法名,也无法对应出IMP
的地址,太抽象了。得来点可以打印调试的解决办法。
2、Cache_t的缓存机制
FBI WARNING:分析很长,结论也不短,如果不看分析可以直接跳最后总结。
书接上文,这里来个偷梁换柱
的办法,将所有的这种类替换成自己的:
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct pp_bucket_t {
SEL _sel;
IMP _imp;
};
struct pp_cache_t {
struct pp_bucket_t * _buckets;
mask_t _mask;
uint16_t _flags;
uint16_t _occupied;
};
struct pp_class_data_bits_t {
uintptr_t bits;
};
struct pp_objc_class {
Class ISA;
Class superclass;
struct pp_cache_t cache; // formerly cache pointer and vtable
struct pp_class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
将这段定义加入到源码之前,再修改后续代码,添加打印部分:
Person *person = [Person alloc];
person.age = 10;
person.nickname = @"pp";
person.height = 180.0;
person.name = @"ppext";
[person laugh];
[person cry];
[person run];
[person jump];
[person doNothing];
struct pp_objc_class *pp_pClass = (__bridge struct pp_objc_class *)([Person class]);
NSLog(@"_occupied:%hu -_mask:%u",pp_pClass->cache._occupied,pp_pClass->cache._mask);
for (mask_t i = 0; i<pp_pClass->cache._mask; i++) {
// 打印获取的 bucket
struct pp_bucket_t bucket = pp_pClass->cache._buckets[i];
NSLog(@"_sel:%@ - _imp:%p",NSStringFromSelector(bucket._sel),bucket._imp);
}
[person run];
[person laugh];
[person doNothing];
然后运行、打印一下:

SEL
方法名和IMP
地址了,而且occupied=2
,mask=15
,而且还会出现空bucket
。现象尽然出来了,那么问题就来了:【一】
、occupied=2
,mask=15
,这两个是什么意思,这个变化是有个什么规律?,打印的数目也是按照mask
数目来打印的,多打印几个试试会不会有什么不一样?【二】
、之前学习操作系统就知道一种缓存的设计算法那就是内存分页或者缓存设计算法中LRU(最近最少使用),按照这样的话应该在jump
后就是run
方法,这里显然不是这种设计方式。所以这个算法是是需要研究的。【三】
、最早调用了laugh
、cry
方法呢,消失了,究竟去哪里了?
下面一个个来研究:
【一】
、针对occupied
和mask
,在源码方法中只看到了两个的get
方法:
mask_t cache_t::mask()
{
uintptr_t maskAndBuckets = _maskAndBuckets.load(memory_order::memory_order_relaxed);
uintptr_t maskShift = (maskAndBuckets & maskMask);
return 0xffff >> maskShift;
}
unsigned cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
mask_t cache_t::occupied()
{
return _occupied;
}
mask
具体什么值不知道,但是必定会是capacity+1
,这个在reallocate
的源码中也可以验证,尝试更改循环打印的数目,数目改多了会报错,所以这个就是mask
应该就是capacity容量-1
。下面就是occupied
的了:
incrementOccupied
方法、insert
方法:
1、setBucketsAndMask
中是将occupied
设置为0。
2、incrementOccupied
中只是单纯的occupied++
。
3、insert
中mask_t newOccupied = occupied() + 1
也是+1。
其他的就不相干或者是旧版本的方法了,通过对比方法代码行数都可以知道,在insert
方法中应该有部分流程,可以通过分析insert
流程来确定两个值的意义。
insert
方法的参数和实现可以知道这是对cache
进行插入操作调用方法,因此通过Cache_t
源码对这个大致流程分析以下:
ALWAYS_INLINE
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);
}
然后,是occupied
直接加1,记为newOccupied
,在cache
空的时候,新申请内存,大小为capacity
,如果cache
不为空就判断newOccupied
+1后是不是小于等于 capacity
的3/4,小于的话直接走完判断,大于的话,在capacity
不为0且时对capacity
现有值翻倍,为0就将capacity
重置为cache
初始大小 1 << 2)
即为0100
=4,再判断capacity
是否大于最大cache
的大小——1 <<16
=0x0001 0000 0000 0000 0000
即2的16次方,如果大于就设为最大值,并重新申请内存。
//最后
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);
}
最后,重点是一个do-while
循环:(由于这里是fastpath
才能进入第二次循环,就按fastpath
流程分析)
m = capacity - 1
,begin = cache_hash(sel, m)
是一个位运算出来的到的值,sel & mask
产生的,由于刚初始化所以i
与begin
等价。
do-while
循环中,第一个判断,先确定i
这个位置的bucket
的SEL
是不是空,为空就表示这个位置是可以插入的,所以对occupied++
,对这个位置的bucket
设置SEL
、IMP
,且直接完成。第二个判断,要插入的SEL
是否和当前位置i
的bucket
相等,表示方法已经插入过,直接完成。循环判断条件,由于是模拟器运行,所以是x86分支,转换一下代码可以得到:(i = ((i+1)&m) != begin
arm64也同样可以转化得到:(i = (i ? i-1 : m)) != begin
,这里分析arm64流程,将如果i
不为0就继续-1
,所以在arm64的情况是从最大index
慢慢循环到最小,也就是递减,为0就直接=m
,也就和begin
相等。
如果循环出来之后仍未插入bucket
,就会走bad_cache
方法。
分析完后发现,明面上除了当前位置i
的bucket
为空了才进行incrementOccupied()
,并未对mask
的直接操作。
还有一个方法没有研究就是 reallocate
方法,这里就不贴出源码了可以知道在reallocate
方法中setBucketsAndMask
方法中将occupied
置为0。所以,捋一捋:

reallocate
有点东西,要具体研究下:
ALWAYS_INLINE
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) {
cache_collect_free(oldBuckets, oldCapacity);
}
}
参数:
1、mask_t oldCapacity
旧的容量;
2、mask_t newCapacity
新的容量;
3、bool freeOld
是否释放旧的buckets
;
先将旧的buckets
取出,然后以新的容量申请新的buckets内存并得到指针newBuckets
。
setBucketsAndMask
是对内存的操作,将occupied
置为0。
判断是否需要删除oldBuckets
,需要就进入cache_collect_free
方法。
static void cache_collect_free(bucket_t *data, mask_t capacity)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
runtimeLock.assertLocked();
#endif
if (PrintCaches) recordDeadCache(capacity);
_garbage_make_room ();
garbage_byte_size += cache_t::bytesForCapacity(capacity);
garbage_refs[garbage_count++] = data;
cache_collect(false);
}
可以看出释放oldBuckets
并不是直接置为nil
,而是先使用_garbage_make_room
装下来,然后通过cache_collect
方法来将_garbage_make_room
清空,大致流程就是这样。
那么这个insert
的流程大致就清楚了,下面跟着lldb
打印来验证一下(由于是在模拟器上调试的,所以部分分支走x86,而且之前在类的结构分析中打印methods
是可以看到set
和get
方法的,所以在这些自定义方法调用之前,必须先将set
方法加入。这样走一边流程:
Step.1
调用setAge()
方法——OldCapacity
为0,newOccupied
为1,——newOccupied + CACHE_END_MARKER
=2>=0所以进入else
——capacity=INIT_CACHE_SIZE=4
,进入到reallocate
方法,新申请capacity
为4的buckets。进入循环,cache_hash(sel,m)
得到i
和begin
。第一个条件b[i].sel() == 0
由于cache
是空的,所以必然会进入,所以occupied
=1,将这个i = setAge&3
这个位置设为setAge()
方法的bucket
,此时,occupied
=1,mask
=3。
Step.2
调用setNickname()
方法——OldCapacity
为4,newOccupied
为2——newOccupied + CACHE_END_MARKER
=3<=4 * 3/4——i=setNickname&3
——进入循环,当前i
和cry()
方法的setNickname&3
不同——occupied
=2,将i = setNickname&3
这个位置设为setNickname()
方法的bucket
——此时,occupied
=2,mask
=3。
Step.3
调用setHeight()
方法——OldCapacity
为4,newOccupied
为3——newOccupied + CACHE_END_MARKER
=4>4 * 3/4——进入else
——capacity=capacity*2=8
——进入reallocate
——occupied
置为0——释放oldBuckets
——i=run&7
——进入循环,当前i
和setHeight()
方法的setHeight&7
不同——occupied
=1,将i = setHeight&7
这个位置设为setHeight()
方法的bucket
——此时,occupied
=1,mask
=7。
Step.4
调用setName()
方法——OldCapacity
为8,newOccupied
为2——newOccupied + CACHE_END_MARKER
=3<=8 * 3/4——i=setName&3
——进入循环,当前i
和setName()
方法的setName&7
不同——occupied
=2,将i = setName&8
这个位置设为setName()
方法的bucket
——此时,occupied
=2,mask
=7。
Step.5
调用laugh()
方法——OldCapacity
为8,newOccupied
为3——newOccupied + CACHE_END_MARKER
=4<=8 * 3/4——i=laugh&7
——进入循环,当前i
和laugh()
方法的laugh&7
不同——occupied
=3,将i = laugh&7
这个位置设为laugh()
方法的bucket
——此时,occupied
=3,mask
=7。
Step.6
调用cry()
方法——OldCapacity
为8,newOccupied
为4——newOccupied + CACHE_END_MARKER
=5<=8 * 3/4——i=cry&7
——进入循环,当前i
和cry()
方法的cry&7
不同——occupied
=4,将i = cry&7
这个位置设为cry()
方法的bucket
——此时,occupied
=4,mask
=7。
Step.7
调用run()
方法——OldCapacity
为8,newOccupied
为5——newOccupied + CACHE_END_MARKER
=6<=8 * 3/4——i=run&7
——进入循环,当前i
和run()
方法的run&7
不同——occupied
=5,将i = run&7
这个位置设为run()
方法的bucket
——此时,occupied
=5,mask
=7。
Step.8
调用jump()
方法——OldCapacity
为8,newOccupied
为6——newOccupied + CACHE_END_MARKER
=7> 8 * 3/4——i=jump&7
——进入else
——capacity=capacity*2=16
——进入reallocate
——occupied
置为0——释放oldBuckets
——i=jump&15
——进入循环,当前i
和jump()
方法的jump&15
不同——occupied
=1,将i = jump&15
这个位置设为jump()
方法的bucket
——此时,occupied
=1,mask
=15。
Step.9
调用doNothing()
方法——OldCapacity
为16,newOccupied
为2——newOccupied + CACHE_END_MARKER
=3<=16 * 3/4——i=doNothing&15
——进入循环,当前i
和doNothing()
方法的doNothing&15
不同——occupied
=2,将i = doNothing&15
这个位置设为doNothing()
方法的bucket
——此时,occupied
=2,mask
=15。
至此,由于最近的一次reallocate
是在jump
方法,而且在x86
模拟器环境下i
值是递增的,所以在16个capacity
的 newBucket
中最顶端的两个就是doNothing
和jump
,之前的方法都被放进_garbage_make_room
给释放了。
这样,之前存在的【一】
、【二】
、【三】
三个问题就全部有了解释,在走了方法调用流程后就了解cache_t
缓存的实现机制,
真相还不止这样,有很多细节都需要挖掘:
在循环的打印中修改一下打印内容,多加一个Person
方便过滤:
NSLog(@"Person _occupied:%hu -_mask:%u",pp_pClass->cache._occupied,pp_pClass->cache._mask);
for (mask_t i = 0; i<pp_pClass->cache._mask; i++) {
// 打印获取的 bucket
struct pp_bucket_t bucket = pp_pClass->cache._buckets[i];
NSLog(@"Person _sel:%@ - _imp:%p",NSStringFromSelector(bucket._sel),bucket._imp);
}
在insert
方法,do-while
循环中加入打印:
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
//加入打印
printf("class:%s sel:%s i=%d capacity=%d occupied=%d\n",cls->mangledName(),(char *)sel,i,capacity,occupied());
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));
command+R
跑起来,以Person
为关键词过滤输出得到:

1、
alloc
方法也是走过insert
的,这里我是没写init
,其实init
方法也会走insert
,alloc
方法通过注销代码调试一下,可以知道并没有进入buckets
且也没有影响occupied
和mask
,但是init
进入了,也会影响这两个值,甚至dealloc
、cxx_destruct
也进入insert
了,这里就不研究了,不然本文更长了-。-。2、
capacity
、occupied
、mask
的值如之前走流程一样没有问题,但是这个i
是通过cache_hash
生成的,是一种哈希算法,生成的第一个i
值是不确定的,这主要是针对mask
和sel
,当然如果这两个都是一样那哈希生成的值也是确定的,这也是为什么程序运行多次,方法bucket
的顺序还是不变的,这里jump
和doNothing
的先后顺序就没那么重要了。
1、Cache_t的结构:
//精简版
struct Cache_t {
struct bucket_t * _buckets;
mask_t _mask;
uint16_t _flags;
uint16_t _occupied;
};
//cache的主要缓存方法都在_buckets指针中
struct bucket_t {
SEL _sel;
IMP _imp;
};
2、Cache_t的缓存机制
2.1、容量capacity
-1 = 掩码mask
,当前存放的方法缓存occupied
;
2.2、容量capacity
:初始容量为4,存放了大致超过3/4的量就会扩容,量翻倍;
2.3、当前存储buckets
量occupied
:每次插入一个bucket
就会加1,但是在扩容时会先置0再加1;
2.4、扩容:重新进行新内存的申请和就内存的释放,旧bucket
会释放,新的bucket
再插入buckets
列表;
2.5、新bucket
插入:以i
为index,初始值是cache_hash
方法以sel&mask
哈希生成的随机值,然后以i = cache_next
的方法进行遍历查找是否可以插入的位置i
,直到可以插入为止occupied
+1;(ps:找到相同sel
就直接return)
2.6、alloc
、init
、dealloc
、cxx_destruct
等系统方法也会走缓存流程,但部分不会存入buckets
列表,可以自己尝试。
至于insert
方法的上层调用cache_fill
是如何被调用的,需要结合runtime
中的objc_msgSend
流程,后续研究~
网友评论