在上篇中我们着重分析了类中cache的方法缓存的插入方式,讲到了buckets(桶),那么本篇就整体对cache的整个流程做一下总结
一.cache_t的成员变量
首先看下源码
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4 当前最大数量
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2 记录当前存储的方法数量
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
};
通过上篇我们脱离源码分析得出:
1._occupied 是记录当前插入存储的新方法数量,核心代码如下
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();//_occupied++; 说白了每次插入一个新的bucket_t _occupied++
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
void cache_t::incrementOccupied()
{
_occupied++;
}
每次在给新的buckets进行set时,就会+1
2._maybeMask 当前可存放的最大数量,核心代码如下
void cache_t::insert(SEL sel, IMP imp, id receiver){
。。。省略
reallocate(oldCapacity, capacity, true);
。。。省略
}
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);
//设置BucketsAndMask 插入空的容器(桶子)
setBucketsAndMask(newBuckets, newCapacity - 1);
//是否清空原来内存
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask){
....省略
_maybeMask.store(newMask, memory_order_release);
....省略
}
可以看到_maybeMask就是扩容后-1
3._bucketsAndMaybeMask 是buckets的首地址,后续获取其他buckets,需要内存平移就是从_bucketsAndMaybeMask开始
示意图:
核心代码如下:
//注释也有说明:bucketsAndMaybeMask是一个低48位的buckets指针
// _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
struct bucket_t *buckets() const;
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);(这里是做数据强转,强转成指针)
}
我们也可以自己测试,代码如下
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson class];
[p say1];
开始lldb调试
2021-06-28 12:26:46.716115+0800 KCObjcBuild[9000:147161] LGPerson say : -[LGPerson say1]
(lldb) p/x pClass
(Class) $0 = 0x00000001000045e0 LGPerson
(lldb) p (cache_t *)0x00000001000045f0
(cache_t *) $1 = 0x00000001000045f0
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4302932400//_bucketsAndMaybeMask的value值
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 3
}
}
_flags = 32808
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001802800000003
}
}
}
}
(lldb) p/x 4302932400 //强转16进制结果和bucket_t * 一样
(long) $3 = 0x00000001007989b0
(lldb) p $2.buckets()
(bucket_t *) $4 = 0x00000001007989b0
通过上面的调试也说明了一点,为什么_maybeMask为最大容量-1,因为需要留一块内存存储(bucket_t *)
二.内存平移获取buckets
还是延续上面,继续LLLD尝试下内存平移
(lldb) p $2.buckets()[1]
(bucket_t) $5 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = (null)
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $2.buckets()+1
(bucket_t *) $6 = 0x00000001007989c0
(lldb) p $2.buckets()+2
(bucket_t *) $7 = 0x00000001007989d0
(lldb) p $2.buckets()+3
(bucket_t *) $8 = 0x00000001007989e0
(lldb) p *$6 //和p $2.buckets()[1] 是一样的
(bucket_t) $10 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = (null)
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p *$7
(bucket_t) $11 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 32640
}
}
}
相信通过上面也完全可以证实,_bucketsAndMaybeMask 是buckets的首地址,通过它进行内存平移的说法了,然后再看下sel,imp
struct bucket_t {
private:
。。。省略
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
。。。省略
public:
。。。省略
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
}
//设置bucket_t
void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};
从上面源码也可以也可以看出怎么获取sel,imp,继续打印
(lldb) p $11.sel()
(SEL) $12 = "say1" //say1出来了
(lldb) p $11.imp(nil, pClass) //第一个参数直接穿nil就行,从上面源码可以看出,返回imp时,有三种情况判断
(IMP) $15 = 0x0000000100003a60 (KCObjcBuild`-[LGPerson say1])
(lldb)
三.问题的延伸
到目前为止,其实cache的内部结构已然很清晰了,包括插入的流程,可能会有人疑问:
- 为什么插入的bucket_t到容量的3/4就开始扩容,这方面其实涉及到数学一些运算,简单来说,0.75是负载因子,这个时候的空间利用率最高,在搭配上哈希函数,可以最大程度的利用好内存,也保证了插入和读取效率
- 什么时候开始插入bucket_t,在此之前又做了什么呢?那么接下来我继续断点调试
首先在cache_t::insert函数调用时断点
可以看到从源码分析的话,从IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
走到log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
然后就是cache_t::insert(SEL sel, IMP imp, id receiver),当然源码中也有更具体的描述
- 在objc_cache.mm文件头部给出的Cache闭环流程
* Cache readers (PC-checked by collecting_in_critical())
* objc_msgSend*
* cache_getImp
*
* Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
* cache_t::copyCacheNolock (caller must hold the lock)
* cache_t::eraseNolock (caller must hold the lock)
* cache_t::collectNolock (caller must hold the lock)
* cache_t::insert (acquires lock)
* cache_t::destroy (acquires lock)
从上面论述也可以看出,如果想要继续探索就得开始探索_objc_msgSend,没关系下期,新的篇章就要开启,冲冲冲!!!