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;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
}
上一部分分析了class中的bits,本篇文章探究类结构中的cache_t,主要从两种方法来探究,lldb,脱离源码结构,这两种方法来探究。
lldb探究
探究的环境如下:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;
- (void)sayHappy;
@end
@implementation Person
- (void)sayHappy{
NSLog(@"%s",__func__);
}
@end
既然是缓存,那么调用之后才会产生,先调用一下
Person *p = [Person alloc];
[p sayHappy];
打印cache如下,p (cache_t *)0x100008428(由于类的结构,偏移16得到)
看到里面有_occupied = 1。
看一下cache_t的结构
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
};
......
public:
// The following four fields are public for objcdt's use only.
// objcdt reaches into fields while the process is suspended
// hence doesn't care for locks and pesky little details like this
// and can safely use these.
unsigned capacity() const;
struct bucket_t *buckets() const;
......
void insert(SEL sel, IMP imp, id receiver);
}
从结构中看到,里面有一个buckets()我们来取出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
public:
static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
....
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
.....
}
}
取出里面的sel和imp,
❓添加一个加号方法,也会在里面吗,我们来测试一下,
Person.h
@interface Person : NSObject
...
+ (void)saySad;
@end
Person.m
@implementation Person
...
+ (void)saySad {
NSLog(@"%s",__func__);
}
...
@end
main.m
...
[Person saySad];
打印一下Person里面的cache_t
_oocupied仍然是1,说明不在Person的cache_t中,那在哪里呢,我们测试一下元类
可以看到
类方法在元类的cache_t里面
脱离源码
我们可以在源码中看到cache_t的数据结构,我们如果把部分数据结构复制出来,然后进行打印,是不是更加直观一些? 我们可以把数据结构复制出来一些 研究对象
@interface Person : NSObject
- (void)sayHappy1;
- (void)sayHappy2;
- (void)sayHappy3;
- (void)sayHappy4;
- (void)sayHappy5;
+ (void)sayno;
@end
@implementation Person
- (void)sayHappy1 {
NSLog(@"__%s__", __func__);
}
- (void)sayHappy2 {
NSLog(@"__%s__", __func__);
}
- (void)sayHappy3 {
NSLog(@"__%s__", __func__);
}
- (void)sayHappy4 {
NSLog(@"__%s__", __func__);
}
- (void)sayHappy5 {
NSLog(@"__%s__", __func__);
}
+ (void)sayno {
}
@end
typedef uint32_t mask_t;
struct dw_bucket_t {
SEL _sel;
IMP _imp;
};
struct dw_cache_t {
struct dw_bucket_t *_buckets;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct dw_class_data_bits_t {
uintptr_t bits;
};
struct dw_objc_class {
Class isa;
Class superclass;
struct dw_cache_t cache; // formerly cache pointer and vtable
struct dw_class_data_bits_t bits;
};
Person *p = [Person alloc];
[p sayHappy1];
[p sayHappy2];
[Person sayno];
struct dw_objc_class* pclass = (__bridge struct dw_objc_class *)(p.class);
NSLog(@"%u_%u", pclass->cache._occupied, pclass->cache._maybeMask);
for (int i = 0; i < pclass->cache._maybeMask; i++) {
struct dw_bucket_t bucket = pclass->cache._buckets[i];
NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
打印结果如下:
由于分配了3个空间,但是有两个方法,打印的时候有时候是空的。
我们调用
3次,结果如下
Person *p = [Person alloc];
[p sayHappy1];
[p sayHappy2];
[p sayHappy3];
/**
2021-06-23 13:45:53.212623+0800 cache_t[5544:349832] 1_7
2021-06-23 13:45:53.212775+0800 cache_t[5544:349832] sayHappy3 - 0x8ad8f
2021-06-23 13:45:53.212852+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.212937+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.213007+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.213077+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.213147+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.213299+0800 cache_t[5544:349832] (null) - 0x0f
**/
❓为什么变成了1_7
我们在多调用几次
Person *p = [Person alloc];
[p sayHappy1];
[p sayHappy2];
[p sayHappy3];
[p sayHappy4];
[p sayHappy2];
/**
2021-06-23 13:48:24.101867+0800 cache_t[5579:352651] 3_7
2021-06-23 13:48:24.102031+0800 cache_t[5579:352651] sayHappy3 - 0x18b20f
2021-06-23 13:48:24.102108+0800 cache_t[5579:352651] (null) - 0x0f
2021-06-23 13:48:24.102197+0800 cache_t[5579:352651] sayHappy4 - 0x18af0f
2021-06-23 13:48:24.102277+0800 cache_t[5579:352651] (null) - 0x0f
2021-06-23 13:48:24.102351+0800 cache_t[5579:352651] (null) - 0x0f
2021-06-23 13:48:24.102445+0800 cache_t[5579:352651] (null) - 0x0f
2021-06-23 13:48:24.102624+0800 cache_t[5579:352651] sayHappy2 - 0x18b10f
(lldb)
**/
我们发现当有3个的时候发生了改变,之前的清空了,容量变大了,新存储了新的值
我们看一下源码。在之前上面的cache_t的源码中有一个insert函数,我们来看一下内部实现。
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
....
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) { //第一次进来走这里
if (!capacity) capacity = INIT_CACHE_SIZE;
// INIT_CACHE_SIZE_LOG2 = 2,
// INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
// 1<<2 所以capacity = 4
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);
}
/**以下是一个hash存值的过程*/
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();// _occupied++;
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);
}
由注释只,第一次进到if (slowpath(isConstantEmptyCache())) { 这个分支里面,我们看下reallocate
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);
//设置buckets和mask
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
我们看一下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);//第一次进来newMask为3
_occupied = 0;//还没有赋值
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}
从上面的代码我们知道第一次进来缓存时: _maybeMask.store(newMask, memory_order_release); _maybeMask = 3,
当后面的在缓存的时候 if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))
mask_t newOccupied = occupied() + 1;
#define CACHE_END_MARKER 1
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
所以上面的条件就变为当前缓存方法的个数+1+1<=3/4分配的空间,即缓存小于容量的3/4时,大于3/4时走else
{ //进行2倍扩容 当为4个方法时capacity = 8
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
所以当有4个方法时capacity = 8, maybeMask = 7;
在reallocate中,第二次进来的时候freeOld为true,便进行一个collect_free操作
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);
}
抹掉老的内存。所以为3的时候,进行了扩容,抹掉了之前的内存,就只有一个函数了。
缓存模拟器模拟器流程图如下:
补充
调用入口
在insert里面添加一个断点,可以得到上面的流程图,但是什么时候触发的lookUpImpOrForward,我们搜到cache_t的时候卡在objc-cache.mm文档中可以看到,有如下一段话
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)
*
* UNPROTECTED cache readers (NOT thread-safe; used for debug info only)
* cache_print
* _class_printMethodCaches
* _class_printDuplicateCacheEntries
* _class_printMethodCacheStatistics
缓存的读取是在objc_msgSend这个流程中进行的吗,接下来我们就来探究objc_msgSend这个流程。
lldb下的扩容情况
表现代码:
@interface Person : NSObject
// isa 8
@property (nonatomic, copy) NSString *name; //8
@property (nonatomic) int age; //4
@property (nonatomic) int score; //4
@property (nonatomic) int score1; //4
- (void)saySomething;
- (void)saySomething1;
- (void)saySomething2;
- (void)saySomething3;
- (void)saySomething4;
@end
@implementation Person
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)saySomething1 {
NSLog(@"%s",__func__);
}
- (void)saySomething2 {
NSLog(@"%s",__func__);
}
- (void)saySomething3 {
NSLog(@"%s",__func__);
}
- (void)saySomething4 {
NSLog(@"%s",__func__);
}
@end
我们用lldb来调用[p saySomething] 然后再看*$1,我们发现如下,
_maybeMask = { std::__1::atomic<unsigned int> = { Value = 7 }
❓这是为什么呢,是在什么时候扩容了吗
先在insert中插入一行打印,看都有哪些方法
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
printf("\n走insert%p--%p--%p\n", sel, imp, receiver);
...
}
/*
走insert0x7fff7b869cce--0x10034ff20--0x1006a3460
走insert0x7fff7b869c33--0x10034fb80--0x1006a3460
走insert0x100367b21--0x100003b10--0x1006a3460
*/
打印p<Person: 0x1006a3460>,我们看到上面0x1006a3460中有三个,分别打印下SEL
在此之前已经有了两个方法class和respondsToSelector,
我们把打印范围缩小,打印下p [p saySomething]之前的所有方法
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (sel ==@selector(saySomething)) {
bucket_t *dw_b = buckets();
for (unsigned i = 0; i < oldCapacity; i++) {
SEL dw_sel = dw_b[i].sel();
IMP dw_imp = dw_b[i].imp(dw_b, nil);
printf("%p--%p--%p\n", dw_sel, dw_imp, &dw_b[i]);
}
printf("isConstantEmptyCache %p -%u-%u-%u---", dw_b, capacity, newOccupied, oldCapacity);
}
if (slowpath(isConstantEmptyCache())) {
看到里面有三个方法,第一个和第三个分别是上面的class和respondsToSelector,但是ox1这个是是什么呢,我们看一下源码:
点开allocateBuckets的代码
bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
....
// 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);
...
return newBuckets;
}
看到在最后会插入一个1,imp指向第一个bucket的首地址;
所以最后一个是0x1--0x10142b360--0x10142b390,当调用[p saySomething]的时候会进行一个扩容。
但是objc的源码环境下 调用第二个方法时:此时缓存一个方法
调用第四个方法时,扩容,只缓存了第三个方法,是在调用第三个方法的时候进行扩容的。
猜测:可能是编译器对于打印做了优化,过滤掉了,系统自动调用的函数。