一、cache数据结构
1、通过源码分析cache
在《OC底层探究之类的底层原理结构》一文中,我们已经知道了类的结构,并且分析了bits
。
现在开始分析cache
,cache
顾名思义就是缓存
!
依旧是objc源码
,打上断点
,通过内存平移0x10
,得到cache
:
发现_maybeMask
和_occupied
的值都为0
,而其他的值都比较大,其内部的结构
和我们之前查阅源码是一致的:
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;
};
//省略方法、全局变量等等,方法在方法区、全局变量在全局区不占用结构体内存
}
__LP64__是指Unix和Unix类的系统(Linux,Mac OS X等等)。
但是cache_t
里面这么多成员变量
,到底是哪个才是缓存
呢?
缓存
不外乎就是增删改查
,在方法里面找相关方法!
在cache_t
这个结构体里面找到了insert
方法,很明显这就是插入方法:
void insert(SEL sel, IMP imp, id receiver);
进入到insert
方法里面,看方法先看return
:
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
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));
发现return
前b[i]
调用了set
方法,那么b
是什么呢:
bucket_t *b = buckets();
再进入到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
//省略方法等
}
发现bucket_t
就有IMP
和SEL
这2个成员变量。
SEL : 类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。
IMP:一个函数指针,保存了方法的地址
bucket_t
会在哪个成员变量里面呢?
很明显在_bucketsAndMaybeMask
里面!
2、图形总结
那么我们就可以得出一个这样的图:
二、cache底层LLDB分析
接下来我们打印一下_bucketsAndMaybeMask
:
(lldb) p $2._bucketsAndMaybeMask
(explicit_atomic<unsigned long>) $3 = {
std::__1::atomic<unsigned long> = {
Value = 4436931456
}
}
发现里面有个Value
,继续打印Value
:
(lldb) p $3.Value
error: <user expression 4>:1:4: no member named 'Value' in 'explicit_atomic<unsigned long>'
$3.Value
~~ ^
获取不到数据怎么办?
找方法!
在cache_t
结构体里面就会发现:
struct bucket_t *buckets() const;
接着调用buckets()
方法:
(lldb) p $2.buckets()
(bucket_t *) $4 = 0x0000000108763380
(lldb) p *$4
(bucket_t) $5 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = (null)
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
这就打印出了我们的_sel
和_imp
!
但是没有值!
因为对象没有使用方法,所以没有缓存!
先使用方法后再去获取:
这次有值了,而且cache_t
里面的_maybeMask
的值为3
,_occupied
的值为1
,和我们的方法数量有点相像,但是SEL
和我们获取的的方法名好像不一样!
接下来继续去bucket_t
里面查找相应的方法:
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, 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
}
可以找到SEL
和IMP
相应的获取方法!
继续打印:
(lldb) p $4.sel()
(SEL) $5 = "printName"
但是imp
方法需要传bucket_t
和Class
,bucket_t
不知道怎么办?
传nil
进行尝试:
(lldb) p $4.imp(nil, pClass)
(IMP) $6 = 0x0000000100003d70 (HObjectBuild`-[HPerson printName])
成功打印了我们使用的方法printName
!
三、脱离源码分析cache
1、自定义源码结构体
如果没有源码的情况呢?我们该怎么去调试呢?
只需要自己去定义相关的结构体即可!
开一个新工程,先自定义objc_class
结构体,并重命名:
struct h_objc_class {
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
和class_data_bits_t
找不到,继续自定义cache_t
结构体:
struct h_cache_t { 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; };};
然后发现explicit_atomic
是未知的,所以我们直接删除。
mask_t
也不知道,去源码查看:
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
所以直接把mask_t
也拿过来!
下面的联合体只需要结构体内的即可,所以可以把_originalPreoptCache
删除,并把结构体拿出来:
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct h_cache_t {
uintptr_t _bucketsAndMaybeMask;
mask_t _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
继续根据源码自定义class_data_bits_t
:
struct h_class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
};
然后friend
这个友类就不需要了:
struct h_class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
};
最后再调整h_objc_class
结构体:
struct h_objc_class {
Class isa;
Class superclass;
struct h_cache_t cache; // formerly cache pointer and vtable
struct h_class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
这样我们的自定义objc_class
结构体就完成了!
最后,我们还需要bucket_t
这个结构体,按照上面的做法:
struct h_bucket_t {
// 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__
uintptr_t _imp;
SEL _sel;
#else
SEL _sel;
uintptr_t _imp;
#endif
};
同时可以把uintptr_t
换为_imp
的真正类型IMP
:
struct h_bucket_t {
// 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__
IMP _imp;
SEL _sel;
#else
SEL _sel;
IMP _imp;
#endif
};
这样自定义源码的结构体就基本完成了!
2、调试
把类强转为自定义的h_objc_class
结构体:
struct h_objc_class * pClass = (__bridge struct h_objc_class *)(p.class);
再打印我们之前感觉和方法数量相像的成员变量的值:
打印出了1
和3
,和之前用lldb
一样!
接下来就要找到_bucketsAndMaybeMask
里的bucket_t
!
我们先看看buckets()
方法:
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
打上断点,发现addr
和_bucketsAndMaybeMask
的值是一样:
说明load
就是获取了_bucketsAndMaybeMask
的值,然后在& bucketsMask
,就得到了bucket_t
的指针地址!
所以我们可以直接把_bucketsAndMaybeMask
换成bucket_t
结构体指针:
struct h_cache_t {
struct h_bucket_t * _buckets;
mask_t _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
这样就不用再去写buckets()
相关方法了!可以省略很多繁琐的步骤!
然后就去获取bucket_t
结构体了:
成功的打印出了我们使用过的方法!
但是如果有多个
方法缓存呢?有该怎么获取呢?
buckets()
方法很明显会返回多个bucket
!但是根据源码返回的是指针
,所以我们是否可以用指针偏移
去获取其他的bucket
呢?
假设cache_t
里面的_occupied
存储的是方法数量
,for循环打印:
发现并没有完全打印出我们使用过的方法!
那我们试试_maybeMask
:
这回就成功的打印出了我们使用过的所有方法!
3、总结
这样就完全脱离了源码,并且调试成功,而且不需要lldb调试,方便修改,简单清晰!
如果有时候下载的源码因为种种原因无法调试的话,可以尝试使用此方法!
以及需要小规模取样的时候也可以尝试使用此方法!
四、cache底层原理分析
1、问题
刚刚我们明明只有调用了2
个方法,但是却在打印出一个空
方法后才能完全打印出来!
- 那么如果我们有
3
个方法呢?
就会发现,首先occupied
变成了1
,maybeMask
变成了7
,而且只打印出了最后一个方法!并且有大量的null
!
- 如果有
类方法
呢?
好像类方法并没有在缓存里面!
- 如果调用了
init
方法呢?
发现init
方法进入了缓存,但是我们并没有重写init
方法啊!这是为什么呢?
2、底层分析
①整理思路
首先先分析_bucketsAndMaybeMask
!
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
很明显_bucketsAndMaybeMask
里面存储着是buckets
和MaybeMask
。
但是他的值却只有1
个,是怎么存储的呢?
根据之前的对buckets()
方法的探索,我们可以推断出_bucketsAndMaybeMask
和isa
类似,也是在位上有存储信息!取信息的时候就进行&
操作!
那么要怎么验证呢?
首先找到切入点!cache
是缓存,必然有写
也有读
!
所以从写
开始探索!
②纵观全局
重新探索cache_t
结构体里的insert
方法:
void insert(SEL sel, IMP imp, id receiver);
insert
方法传了3
个参数,SEL
方法编号,IMP
方法地址,receiver
接收者即调用者!
进入到insert
方总览:
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
// Never cache before +initialize is done
if (slowpath(!cls()->isInitialized())) {
return;
}
if (isConstantOptimizedCache()) {
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
ASSERT(sel != 0 && cls()->isInitialized());
// Use the cache as-is if until we exceed our expected fill ratio.
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 <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
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.
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
}
③首次执行
1、先看2个赋值:
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
先是创建了一个newOccupied
,即已使用的缓存数量,再看看occupied()
方法:
mask_t cache_t::occupied() const
{
return _occupied;
}
发现就获取_occupied
!
在来看看capacity()
方法:
unsigned cache_t::capacity() const
{
return mask() ? mask()+1 : 0;
}
mask_t cache_t::mask() const
{
return _maybeMask.load(memory_order_relaxed);
}
这里oldCapacity
和capacity
都为_maybeMask
的值 + 1,或为0!
2、看判断逻辑:
如果是第一次缓存,则进入空缓存判断:
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
slowpath:大概率不会执行
fastpath:大概率会执行
首次插入_maybeMask
为0
,所以capacity
被赋值为INIT_CACHE_SIZE
,进入INIT_CACHE_SIZE
:
/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
enum {
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
// When we have a cache end marker it fills a bucket slot, so having a
// initial cache size of 2 buckets would not be efficient when one of the
// slots is always filled with the end marker. So start with a cache size
// 4 buckets.
INIT_CACHE_SIZE_LOG2 = 2,
#else
// Allow an initial bucket size of 2 buckets, since a large number of
// classes, especially metaclasses, have very few imps, and we support
// the ability to fill 100% of the cache before resizing.
INIT_CACHE_SIZE_LOG2 = 1,
#endif
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
MAX_CACHE_SIZE_LOG2 = 16,
MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2),
FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};
可以得出INIT_CACHE_SIZE
为1
左移2
位,即为4
,所以capacity
为4
!
在看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) {
collect_free(oldBuckets, oldCapacity);
}
}
先是获取旧的buckets
,再创建新的buckets
,然后去设置BucketsAndMask
!
先看allocateBuckets
方法:
bucket_t *cache_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(bytesForCapacity(newCapacity), 1);
bucket_t *end = 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>(newBuckets, (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>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
这里是调用了calloc
方法,即开辟了内存
,看看bytesForCapacity
方法:
size_t cache_t::bytesForCapacity(uint32_t cap)
{
return sizeof(bucket_t) * cap;
}
因为参数newCapacity
为4
,bucket_t
里面为SEL
和IMP
,所以开辟了4 * 16 = 64
字节内存!
在看end
,调用了endMarker
方法:
bucket_t *cache_t::endMarker(struct bucket_t *b, uint32_t cap)
{
return (bucket_t *)((uintptr_t)b + bytesForCapacity(cap)) - 1;
}
这里是通过内存平移得到开辟的内存的最后16字节
的内存地址
!
然后在end
处插入了一个1
,以及新Buckets
的首地址
!
再进入到setBucketsAndMask
方法:
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
// objc_msgSend uses mask and buckets with no locks.
// It is safe for objc_msgSend to see new buckets but old mask.
// (It will get a cache miss but not overrun the buckets' bounds).
// It is unsafe for objc_msgSend to see old buckets and new mask.
// Therefore we write new buckets, wait a lot, then write new mask.
// objc_msgSend reads mask first, then buckets.
#ifdef __arm__
// ensure other threads see buckets contents before buckets pointer
mega_barrier();
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);
// ensure other threads see new buckets before new mask
mega_barrier();
_maybeMask.store(newMask, memory_order_relaxed);
_occupied = 0;
#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;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}
先是_bucketsAndMaybeMask
把newBuckets
转为指针地址存入,再是_maybeMask
存入newMask
,newMask
是newCapacity - 1
,newCapacity
为4
,所以_maybeMask
为3
。
这也是为什么只有1
个缓存方法的时候_maybeMask
为3
的原因!
同时这时newBuckets
里还没有方法,所以_occupied
为0
!
3、方法插入Buckets中:
bucket_t *b = buckets(); //取出刚刚存入的bucket_t
mask_t m = capacity - 1; //4 - 1 = 3
mask_t begin = cache_hash(sel, m); //通过运算得到开始位置
mask_t i = begin;
//----以下是cache_hash方法----
// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.
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);
}
然后进行查找空位
进行插入操作:
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
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));
先进入do
,看第一个if
,就是取刚刚创建的bucket_t
里的sel
,然后判断sel
是否有值,因为是刚创建的所有大多数情况没有值!进入第一个if
!
再看incrementOccupied()
方法:
void cache_t::incrementOccupied()
{
_occupied++;
}
这就是_occupied++
!所以确定_occupied
为方法缓存数量!
然后就调用bucket_t
的set方法传入b
、sel
、imp
、cls
:
template<Atomicity atomicity, IMPEncoding impEncoding>
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls)
{
ASSERT(_sel.load(memory_order_relaxed) == 0 ||
_sel.load(memory_order_relaxed) == newSel);
// objc_msgSend uses sel and imp with no locks.
// It is safe for objc_msgSend to see new imp but NULL sel
// (It will get a cache miss but not dispatch to the wrong place.)
// It is unsafe for objc_msgSend to see old imp and new sel.
// Therefore we write new imp, wait a lot, then write new sel.
uintptr_t newIMP = (impEncoding == Encoded
? encodeImp(base, newImp, newSel, cls)
: (uintptr_t)newImp);
if (atomicity == Atomic) {
_imp.store(newIMP, memory_order_relaxed);
if (_sel.load(memory_order_relaxed) != newSel) {
#ifdef __arm__
mega_barrier();
_sel.store(newSel, memory_order_relaxed);
#elif __x86_64__ || __i386__
_sel.store(newSel, memory_order_release);
#else
#error Don't know how to do bucket_t::set on this architecture.
#endif
}
} else {
_imp.store(newIMP, memory_order_relaxed);
_sel.store(newSel, memory_order_relaxed);
}
}
先是创建newIMP
,判断是否需要编码
!
如果不用编码则直接赋值传进来的imp
。
如果需要编码则进行编码encodeImp
:
// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH 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(base, 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
}
编码的本质就是将imp
和class
进行异或
操作!
编码完成后判断是否是原子
操作!
是的话判断当前imp
是否和新imp
一样,不一样才进行存储!
不是的话直接存储!
第二个if
就是判断如果bucket_t
里面的sel
和当前的sel
一样则不用处理!
再进入到while
!
先看cache_next
方法:
#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
#error unexpected configuration
#endif
依旧是通过运算获取hash
地址!找到空位再执行do
语句!
④再次执行
回到开头的判断:
// Use the cache as-is if until we exceed our expected fill ratio.
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 <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
//----以下为cache_fill_ratio方法----
// Historical fill ratio of 75% (since the new objc runtime was introduced).
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
第二次执行的时候,newOccupied
为_occupied + 1 = 2
,capacity
为_maybeMask + 1 = 4
。
newOccupied + CACHE_END_MARKER = 2 + 1 = 3;
cache_fill_ratio(capacity) = cache_fill_ratio(4) = 3;
即进入到第二个if
,直接进入后面的代码!
所以第二次执行的时候后面的和第一次一样,在bucket_t
里面插入新的方法即可!
⑤第三次执行
第三次执行的时候,newOccupied
为_occupied + 1 = 3
,capacity
为_maybeMask + 1 = 4
。
newOccupied + CACHE_END_MARKER = 3 + 1 = 4;
cache_fill_ratio(capacity) = cache_fill_ratio(4) = 3;
正常情况,即进入到最后一个else
,执行reallocate
方法!
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;//2 * 4 = 8
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
在reallocate
方法里面,重新创建新的bucket_t
!因为参数传了true
,所以释放旧的bucket_t
:
/***********************************************************************
* cache_t::collect_free. Add the specified malloc'd memory to the list
* of them to free at some later point.
* size is used for the collection threshold. It does not have to be
* precisely the block's size.
* Cache locks: cacheUpdateLock must be held by the caller.
**********************************************************************/
void cache_t::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_t::collectNolock(false);
}
这里调用了系统的垃圾栈方法继续释放内存!
而且在setBucketsAndMask
方法中会把_occupied
赋值为0
,_maybeMask
赋值为7
!
所以当我们执行了3
个方法的时候,_occupied
为1
,_maybeMask
为7。
因为旧方法被释放,只有新方法被存入!所以会有大量的null
!
为什么要把旧的方法释放,不移到新的内存空间呢?因为开辟内存并不是在原有的内存地址进行扩张,而是重新开辟新的内存空间!而把旧方法移到新的内存空间会非常消耗性能,所以直接释放是最高效的!