类的结构图如下
之前探索分析了isa
和bits
,下面来看看cache
,它的偏移是16个字节0x10
一. 通过LLDB打印调用方法之后cache的变化情况
cache_t
的成员以及重要的方法:
struct cache_t {
private:
explicit_atomic _bucketsAndMaybeMask; // 8
union {
struct {
explicit_atomic _maybeMask; // 4
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2
};
explicit_atomic _originalPreoptCache; // 8
};
...
public:
// 获取哈希表首地址,哈希表的大小是: _maybeMask + 1
struct bucket_t *buckets() const;
// 插入数据到哈希表
void insert(SEL sel, IMP imp, id receiver);
...
};
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 _imp;
explicit_atomic _sel;
#else
explicit_atomic _sel;
explicit_atomic _imp;
#endif
...
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
}
...
};
下面我们通过运行测试代码来看看cache的变化,测试代码如下:
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;
- (void)saySomething;
- (void)sayHello;
- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson class];
[p say1];
[p say2];
[p say3];
[p say4];
[p say5];
NSLog(@"%@",pClass);
}
return 0;
}
我们先在say1
断点停下,输出:
(lldb) p/x pClass
(Class) $0 = 0x0000000100008468 LGPerson
(lldb) p (cache_t*)0x0000000100008478
(cache_t *) $1 = 0x0000000100008478
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic = {
Value = 4298515392
}
}
= {
= {
_maybeMask = {
std::__1::atomic = {
Value = 0
}
}
_flags = 32808
_occupied = 0
}
_originalPreoptCache = {
std::__1::atomic = {
Value = 0x0000802800000000
}
}
}
}
(lldb)
cache为空_maybeMask == 0
继续在say2
打断点,输出:
2021-06-27 19:07:12.822270+0800 KCObjcBuild[59475:773940] -[LGPerson say1]
(lldb) p *$1
(cache_t) $3 = {
_bucketsAndMaybeMask = {
std::__1::atomic = {
Value = 4317150992
}
}
= {
= {
_maybeMask = {
std::__1::atomic = {
Value = 3
}
}
_flags = 32808
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic = {
Value = 0x0001802800000003
}
}
}
}
(lldb) p $3.buckets()[0].sel()
(SEL) $4 = (null)
(lldb) p $3.buckets()[1].sel()
(SEL) $5 = "say1"
(lldb) p $3.buckets()[2].sel()
(SEL) $6 = (null)
(lldb)
现在哈希表大小为_maybeMask + 1 = 4
,有效条目为_occupied = 1
,我们打印了三条记录,因为哈希表最后位置保存END_MARKER
结束标记
END_MARKER是sel = 1, imp = 哈希表首地址,
arm下 imp = 哈希表首地址 - 1 的位置
,arm64下不设置结束标记
继续在say3
打断点,输出:
2021-06-27 19:20:33.579398+0800 KCObjcBuild[59475:773940] -[LGPerson say2]
(lldb) p *$1
(cache_t) $7 = {
_bucketsAndMaybeMask = {
std::__1::atomic = {
Value = 4317150992
}
}
= {
= {
_maybeMask = {
std::__1::atomic = {
Value = 3
}
}
_flags = 32808
_occupied = 2
}
_originalPreoptCache = {
std::__1::atomic = {
Value = 0x0002802800000003
}
}
}
}
(lldb) p $3.buckets()[0].sel()
(SEL) $8 = (null)
(lldb) p $3.buckets()[1].sel()
(SEL) $9 = "say1"
(lldb) p $3.buckets()[2].sel()
(SEL) $10 = "say2"
(lldb)
现在哈希表大小为_maybeMask + 1 = 4
,有效条目为_occupied = 2
继续在say4
打断点,输出:
2021-06-27 19:23:48.570805+0800 KCObjcBuild[59475:773940] -[LGPerson say3]
(lldb) p *$1
(cache_t) $11 = {
_bucketsAndMaybeMask = {
std::__1::atomic = {
Value = 4311833248
}
}
= {
= {
_maybeMask = {
std::__1::atomic = {
Value = 7
}
}
_flags = 32808
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic = {
Value = 0x0001802800000007
}
}
}
}
(lldb) p $11.buckets()[0].sel()
(SEL) $12 = (null)
(lldb) p $11.buckets()[1].sel()
(SEL) $13 = (null)
(lldb) p $11.buckets()[2].sel()
(SEL) $14 = (null)
(lldb) p $11.buckets()[3].sel()
(SEL) $15 = "say3"
(lldb) p $11.buckets()[4].sel()
(SEL) $16 = (null)
(lldb) p $11.buckets()[5].sel()
(SEL) $17 = (null)
(lldb) p $11.buckets()[6].sel()
(SEL) $18 = (null)
(lldb)
现在哈希表大小为_maybeMask + 1 = 8
,有效条目为_occupied = 1
,哈希表扩容为8
,say1
和say2
被清除掉,表中只有say3
,说明扩容的时候会清除cache
扩容的条件是:如果
插入了新条目之后,有效条目数+1,大于哈希表容量的3/4
,这就是著名的3/4扩容
。arm64是7/8扩容
扩容的大小是:当前大小的两倍
继续say5
调用处断点,输出:
2021-06-27 19:41:16.561423+0800 KCObjcBuild[59475:773940] -[LGPerson say4]
(lldb) p *$1
(cache_t) $19 = {
_bucketsAndMaybeMask = {
std::__1::atomic = {
Value = 4311833248
}
}
= {
= {
_maybeMask = {
std::__1::atomic = {
Value = 7
}
}
_flags = 32808
_occupied = 2
}
_originalPreoptCache = {
std::__1::atomic = {
Value = 0x0002802800000007
}
}
}
}
(lldb) p $19.buckets()[0].sel()
(SEL) $20 = "say4"
(lldb) p $19.buckets()[1].sel()
(SEL) $21 = (null)
(lldb) p $19.buckets()[2].sel()
(SEL) $22 = (null)
(lldb) p $19.buckets()[3].sel()
(SEL) $23 = "say3"
(lldb) p $19.buckets()[4].sel()
(SEL) $24 = (null)
(lldb) p $19.buckets()[5].sel()
(SEL) $25 = (null)
(lldb) p $19.buckets()[6].sel()
(SEL) $26 = (null)
(lldb)
最后是调用say5
之后,输出:
2021-06-27 19:46:22.875604+0800 KCObjcBuild[59475:773940] -[LGPerson say5]
(lldb) p *$1
(cache_t) $27 = {
_bucketsAndMaybeMask = {
std::__1::atomic = {
Value = 4311833248
}
}
= {
= {
_maybeMask = {
std::__1::atomic = {
Value = 7
}
}
_flags = 32808
_occupied = 3
}
_originalPreoptCache = {
std::__1::atomic = {
Value = 0x0003802800000007
}
}
}
}
(lldb) p $27.buckets()[0].sel()
(SEL) $28 = "say4"
(lldb) p $27.buckets()[1].sel()
(SEL) $29 = (null)
(lldb) p $27.buckets()[2].sel()
(SEL) $30 = (null)
(lldb) p $27.buckets()[3].sel()
(SEL) $31 = "say3"
(lldb) p $27.buckets()[4].sel()
(SEL) $32 = (null)
(lldb) p $27.buckets()[5].sel()
(SEL) $33 = "say5"
(lldb) p $27.buckets()[6].sel()
(SEL) $34 = (null)
(lldb) p $27.buckets()[7].sel()
(SEL) $35 = ""
(lldb)
二. cache插入代码分析
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
...
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1; // 1+1
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) { // 第一次进入
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//4
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) { // 没有达到扩容条件
// arm32,x86_64,i386,不是LP64的arm64,是 3/4 扩容
// arm64并且是LP64 是 7/8扩容,最新的iPhone,iPad
// 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 {// 4*2 = 8
// 扩容
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; // 4-1=3
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(); // occupied+1
b[i].set(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
}
第一次进入给哈希表分配
4个
条目空间,然后插入
if (slowpath(isConstantEmptyCache())) { // 第一次进入
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//4
reallocate(oldCapacity, capacity, /* freeOld */false);
}
没有达到扩容条件直接进入插入算法。arm32, x86_64,i386和非LP64的arm64,扩容条件是
newOccupied + 1 <= 3/4容积
;LP64的arm64扩容条件是newOccupied <= 7/8容积
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.
}
如果允许满容积扩容,则cache没有满就直接进入插入算法
#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 {// 4*2 = 8
// 扩容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
扩容的代码如下:
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);
}
}
#if CACHE_END_MARKER
bucket_t *cache_t::endMarker(struct bucket_t *b, uint32_t cap)
{
return (bucket_t *)((uintptr_t)b + bytesForCapacity(cap)) - 1;
}
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(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(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
#else
bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
if (PrintCaches) recordNewCache(newCapacity);
return (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
}
#endif
每次扩容都在表的最后位置设置一个结束标志,
sel = 1, imp = 哈希表首地址
,arm下 imp = 哈希表首地址 - 1 的位置
,arm64并且是LP64的架构不设置结束标记
// End marker's sel is 1 and imp points to the first bucket.
end->set(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
arm64 && LP64
bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
if (PrintCaches) recordNewCache(newCapacity);
return (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
}
扩容的哈希表是一片新分配的内存区,原来的内存区释放,并丢弃之前cache里的所有数据
插入算法是先用
sel
和capacity - 1
算出起始哈希位置
mask_t m = capacity - 1; // 4-1=3
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
哈希函数
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);
}
找到空位置就插入新条目
if (fastpath(b[i].sel() == 0)) { // 当前位置为空,可以存储
incrementOccupied(); // occupied+1
b[i].set(b, sel, imp, cls()); // 存入数据
return;
}
找不到就用
之前的哈希值
和capacity - 1
再做一次哈希,如果不等于起始的哈希位置就继续找空位置
#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
arm64的查找方向是
i-1
向前查找,其他架构是i+1
向后查找,我们现在新买的iphone设备都是arm64架构的
补充一点,arm64的
CACHE_END_MARKER = 0
,而且它的扩容条件是7/8扩容
// 非arm64, mac, 模拟器
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
// 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;
}
// arm64, 但是不是Unix, Linux系统
#elif __arm64__ && !__LP64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
// 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;
}
// arm64并且是Unix, Linux系统,最新的iPhone和iPad都是这个架构
#elif __arm64__ && __LP64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
// Allow 87.5% fill ratio in the fast path for all cache sizes.
// Increasing the cache fill ratio reduces the fragmentation and wasted space
// in imp-caches at the cost of potentially increasing the average lookup of
// a selector in imp-caches by increasing collision chains. Another potential
// change is that cache table resizes / resets happen at different moments.
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 7 / 8;
}
// Allow 100% cache utilization for smaller cache sizes. This has the same
// advantages and disadvantages as the fill ratio. A very large percentage
// of caches end up with very few entries and the worst case of collision
// chains in small tables is relatively small.
// NOTE: objc_msgSend properly handles a cache lookup with a full cache.
#define CACHE_ALLOW_FULL_UTILIZATION 1
#else
#error unknown architecture
#endif
老的iPhone设备,mac和模拟器都是3/4扩容,新的iPhone设备是7/8扩容
最新的真机扩容是不会设置哈希表结束标记的,只有老的真机,mac,和模拟器会设置