iOS-OC底层06:cache方法缓存
前沿
对象的底层实现是结构体objc_object,类的底层实现是结构体objc_class,因为objc_class继承自objc_object(c++中结构体是可以继承的),所以类也是特殊的对象。objc_object有成员变量isa,内部实现通过一个共同体isa_t,isa_t下有一个结构体,里面保存对象所属的类等等信息在底层03可以详细了解。在04可以了解方法属性等信息的详细情况,在这篇中我们详细了解一下,方法缓存。
通过内存偏移访问cache内存
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
}
struct objc_class : objc_object {
// 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
}
我们可以通过类的首地址偏移16字节访问到objc_class的cache成员。为什么是16字节呢?我们可以看到cache前面有变量superclass占8字节,但是objc_class继承自objc_object,objc_object有一个成员变量isa也是占8字节。
既然研究类的方法缓存,所以我们先声明一个类
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end
@implementation LGPerson
- (void)sayHello{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayCode{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayMaster{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayNB{
NSLog(@"LGPerson say : %s",__func__);
}
+ (void)sayHappy{
NSLog(@"LGPerson say : %s",__func__);
}
@end
//创建对象
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson class];
// p.lgName = @"Cooci";
// p.nickName = @"KC";
// 缓存一次方法 sayHello
// 4
[p sayHello];
[p sayCode];
[p sayMaster];
我们通过内存偏移访问cache
(lldb) p/x [LGPerson class]
(Class) $0 = 0x0000000100002298 LGPerson
//0x0000000100002298偏移16字节变为0x00000001000022a8,因
//为0x00000001000022a8是cache_t类型,类型转换打印结果。
(lldb) p (cache_t*)0x00000001000022a8
(cache_t *) $1 = 0x00000001000022a8
我们在调用实例方法之前和之后打印cache_t信息,
//调用实例方法之前
(lldb) p *$0
(cache_t) $1 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x000000010032e410 {
_sel = {
std::__1::atomic<objc_selector *> = (null)
}
_imp = {
std::__1::atomic<unsigned long> = 0
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 0
}
_flags = 32804
_occupied = 0
}
//调用实例方法之后
(lldb) p *$0
(cache_t) $2 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x000000010075f880 {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 11928
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 3
}
_flags = 32804
_occupied = 1
}
可知_occupied前后方法方法了变化。
分析我们在源码中全局搜索_occupied,发现只有在incrementOccupied中有对_occupied的++,我们在全局搜索incrementOccupied,发现只有void cache_t::insert调用,我们通过打断点可知当方法调用的时候会调用cache_t::insert。
探究cache_t::insert
开辟内存的探究
在cache_t::insert中,我们通过阅读代码可知在下面代码是对开辟内存的处理
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 <= capacity / 4 * 3)) { // 4 3 + 1 bucket cache_t
// Cache is less than 3/4 full. Use it as-is.
}
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 扩容两倍 4
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true); // 内存 库容完毕
}
1.第一次调用实例方法
第一次调用实例方法会有第一个if,capacity = INIT_CACHE_SIZE,设置capacity为4。
2.不重新分配内存的情况
当新的占位标记加上最后空留的标记1小于总分配的空间的4分子3时不做处理
3.需要重新开辟内存
当不满足1,2时,需要扩容。当capacity不为空时,扩容为capacity的二倍,当capacity为空时,扩容为4.扩容不是无限制的,最大扩容为2的16次方就是65536,当扩容大于65536时,设置扩容为65536。
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);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
}
}
1.获取之前的buckets()
2.分配新的内存allocateBuckets
bucket_t *allocateBuckets(mask_t newCapacity)
{
bucket_t *newBuckets = (bucket_t *)
calloc(cache_t::bytesForCapacity(newCapacity), 1);
bucket_t *end = cache_t::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>((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>((SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
分配新空间的内存,最后的一个位置设置标记。
3.设置Buckets和Mask setBucketsAndMask
用新的值覆盖旧的值
4.垃圾回收cache_collect_free
static void cache_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_collect(false);
}
_garbage_make_room:确定有足够的内存保存需要释放的对象
garbage_byte_size:计算一共释放的内存大小。
garbage_refs[garbage_count++] = data;把要释放的对象保存起来
cache_collect(false);在合适的时候释放,false不会立即释放
方法存到缓存
写入的代码如下
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 because the
// minimum size is 4 and we resized at 3/4 full.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(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));
cache_t::bad_cache(receiver, (SEL)sel, cls);
1.获取bucket数组:bucket_t *b = buckets();
2.获取哈希后的值:通过mask对方法sel进行哈希得出begin
3.写入bucket数组中
写入到bucket数组是一个循环,我们先判断bucket数组中begin位置的bucket_t的sel方法是不是为0,如果是0,则写入到该位置的bucket_t中并返回,如果该位置的sel方法等于我们要写入的sel方法就不用重新写入并返回。如果都不满足前面两种情况,则对i值进行再hash进行下一个循环。
因为hash的结果没有规律,所以方法写入到缓存的位置也没有规律.
因为会存在扩容,所以扩容前写入到缓存的方法会被释放.
cache的流程图如下
Cooci 关于Cache_t原理分析图.png