已知类是objc_class结构体类型,之前的文章中已经分析了bits,本篇文章将介绍一下cache
一、源码环境下分析
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA; isa指针来源于objc_object 占8个字节
Class superclass; // Class 为结构体指针,占8个字节
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
如图可知objc_class偏移16个字节则为cache的地址空间。首先在源码环境下去调试验证
已知有个名为JPerson的类,接下来去lldb控制台来打印JPerson类的数据。
(lldb) p/x pClass
(Class) $0 = 0x00000001000044c0 JPerson
(lldb) p/x 0x00000001000044c0 + 0x10
(long) $1 = 0x00000001000044d0
(lldb) p (cache_t *)0x00000001000044d0
(cache_t *) $2 = 0x00000001000044d0
(lldb) p *$2
(cache_t) $3 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4298433376
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 0
}
}
_flags = 32808
_occupied = 0
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000802800000000
}
}
}
}
struct cache_t {
private:
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
};
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
uintptr_t imp = _imp.load(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(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
}
如代码所示,对$2取值可以得到一个结构体类型。通过查看objc源码可知,结构体为cache_t结构体。
(lldb) p $3._bucketsAndMaybeMask
(explicit_atomic<unsigned long>) $4 = {
std::__1::atomic<unsigned long> = {
Value = 4298433376
}
}
(lldb) p $4.Value
error: <user expression 5>:1:4: no member named 'Value' in 'explicit_atomic<unsigned long>'
$4.Value
~~ ^
(lldb) p $3._originalPreoptCache
(explicit_atomic<preopt_cache_t *>) $5 = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000802800000000
}
}
(lldb) p $5.Value
error: <user expression 7>:1:4: no member named 'Value' in 'explicit_atomic<preopt_cache_t *>'
$5.Value
~~ ^
通过打印cache_t结构中的 _bucketsAndMaybeMask 和 _originalPreoptCache 发现Value都是不存在的,那么cache_t中的数据存在了哪里呢?这里通过阅读源码的cache_t 可知在结构体中的buckets的类型为bucket_t类型的结构体指针。而结构体bucket_t中存储的是sel和imp。
struct bucket_t *buckets() const;
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
那么取buckets来看一下内容
(lldb) p $3.buckets()
(bucket_t *) $7 = 0x000000010034e360
(lldb) p *$7
(bucket_t) $8 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = (null)
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
取出bucket_t之后发现sel 和 imp都是空的。这里是因为初始化JPerson之后,并没有执行方法,所以并没有缓存进来,执行
(lldb) p [p saySomething]
2021-06-30 18:39:08.561194+0800 KCObjcBuild[15414:540423] -[JPerson saySomething]
重新走一遍上述流程
(lldb) p [p saySomething]
2021-06-30 23:20:16.366619+0800 KCObjcBuild[16917:745477] -[JPerson saySomething]
(lldb) p/x pClass
(Class) $0 = 0x00000001000044c0 JPerson
(lldb) p (cache_t *)0x00000001000044d0
(cache_t *) $1 = 0x00000001000044d0
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4302648560
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7
}
}
_flags = 32808
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001802800000007
}
}
}
}
(lldb) p $2.buckets()
(bucket_t *) $3 = 0x00000001007534f0
(lldb) p *$3
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 30816
}
}
}
(lldb) p $4.sel()
(SEL) $5 = "saySomething"
如代码所示,get $4的sel之后可以得到saySomething。已知是对象p调用了一个saySomething方法,如果我们再调用一个方法呢
(lldb) p [p doSomething]
2021-06-30 23:28:08.085634+0800 KCObjcBuild[16996:751214] -[JPerson doSomething]
那么doSomething 应该如何取出呢,重复上述流程,
(lldb) p $2.buckets()
(bucket_t *) $3 = 0x000000010121aa60
(lldb) p *$3
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 3376648
}
}
}
(lldb) p $4.sel()
(SEL) $5 = "respondsToSelector:"
如代码所示,$4的sel突然变成了respondsToSelector:方法,doSomething 和saySomething都不见了。
(lldb) p $2.buckets()[1]
(bucket_t) $6 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 30856
}
}
}
(lldb) p $6.sel()
(SEL) $7 = "saySomething"
这里说明一下buckets 其实是一个哈希链表结构,通过取buckets 的第一个元素,再打印$6的sel得到了saySomething方法
(lldb) p $2.buckets()[5]
(bucket_t) $11 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 30808
}
}
}
(lldb) p $11.sel()
(SEL) $12 = "doSomething"
通过此段代码可知,读取imp的方式
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)
(lldb) p $11.imp(nil,pClass)
(IMP) $13 = 0x0000000100003c80 (KCObjcBuild`-[JPerson doSomething])
上述的调试是在源码环境下使用了lldb的方式去调试的,如果实在非源码环境下如何去调试呢。
二、非源码环境下分析
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct sn_bucket_t {
SEL _sel;
IMP _imp;
};
struct sn_cache_t {
struct sn_bucket_t *_bukets; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
struct sn_class_data_bits_t {
uintptr_t bits;
};
// cache class
struct sn_objc_class {
Class isa;
Class superclass;
struct sn_cache_t cache; // formerly cache pointer and vtable
struct sn_class_data_bits_t bits;
};
如代码所示,将objc_class结构采用伪代码的形式,自定义一份。
SNPerson *p = [SNPerson alloc];
Class pClass = p.class; // objc_clas
[p say1];
struct sn_objc_class *sn_class = (__bridge struct sn_objc_class *)(pClass);
NSLog(@"%hu - %u",sn_class->cache._occupied,sn_class->cache._maybeMask);
for (mask_t i = 0; i<sn_class->cache._occupied; i++) {
struct sn_bucket_t bucket = sn_class->cache._bukets[I];
NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
lldb输出的结果为
2021-07-06 13:00:52.781654+0800 003-cache_t脱离源码环境分析 [67319:589187] 2 - 3
2021-07-06 13:00:52.781733+0800 003-cache_t脱离源码环境分析[67319:589187] say1 - 0x5750f
2021-07-06 13:00:52.781823+0800 003-cache_t脱离源码环境分析[67319:589187] class - 0x7ffe6e4c7145f
通过输出可知_occupied 和 _maybeMask分别为2和 3,以_occupied为循环体条件输出了两个方法,say1和class。那么_occupied和_maybeMask分别是什么呢?这里我们将回归源码去查询。
void insert(SEL sel, IMP imp, id receiver);
void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
void destroy();
void eraseNolock(const char *func);
查看cache_t结构体,发现有个insert方法。查看insert方法

isConstantEmptyCache的方法
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),
};
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//4
reallocate(oldCapacity, capacity, /* freeOld */false);
}
// newCapacity = 4
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);
}
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
// 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;
}
已知我们的真机环境是arm64,所以INIT_CACHE_SIZE_LOG2等于2。INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),那么INIT_CACHE_SIZE等于4。通过reallocate里面的setBucketsAndMask方法可知,开辟的newBuckets是存在了_bucketsAndMaybeMask中,而_maybeMask则是存储了开辟的桶子大小减一。所以这就是前面伪代码输出的为什么_maybeMask为3的原因。而此时_occupied依然为0。再分析插入的代码。
bucket_t *b = buckets();
mask_t m = capacity - 1; // 4-1=3
mask_t begin = cache_hash(sel, m); // 通过hash来得到当前sel的下标
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));
void cache_t::incrementOccupied()
{
_occupied++;
}
当首次调用方法时fastpath(b[i].sel() == 0) 是成立的。调用incrementOccupied()方法可以看到_occupied++。所以这就是前面伪代码输出的为什么_occupied为1的原因。
总结
当首次调用方法时,会先开辟一个大小为4的内存空间,_maybeMask的大小为桶子的大小减一。如果要插入的方法不存在时,则_occupied会加1。