前言
本文主要探索cache_t * cache
结构内容,分析它在类的结构中扮演了什么样的角色。
通过前面OC底层原理04的介绍,我们对类的结构有了一个清晰的了解,总结如下
在OC底层原理03— isa探究中,通过objc_object
源码 对isa进行了探究,OC底层原理04对bits做了探索。现在来探究cache
。cache
字面意思--缓存很好理解,那它缓存了什么内容呢?已经它缓存的结构又是怎么样呢?
缓存: 缓存了增删改查操作 ,目的为了操作安全。每调用一次方法,就缓存下来。
从源码出发
我们来看看cache_t
源码
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
// explicit_atomic:显示原子性,为了在缓存增删改查操作的操作安全,提高线程的安全性。
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets; //arm64与mac环境不一样,将二者结合在一起使用,目的是优化,提高效率。
mask_t _mask_unused;
// How much the mask is shifted by.
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1; // 类似isa的面具
// Ensure we have enough bits for the buckets pointer.
static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
static constexpr uintptr_t maskBits = 4;
static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags; //
#endif
uint16_t _occupied; //
结构体
CACHE_MASK_STORAGE
的三种情况:
CACHE_MASK_STORAGE_OUTLINED
: 模拟器 \ macOS 环境
CACHE_MASK_STORAGE_HIGH_16
: 真机环境
CACHE_MASK_STORAGE_LOW_4
: 不是64位的真机环境
其他属性:
bucke_t
: 装着imp
,sel
mask
:类似isa中的mask(面具)
_flags
: 位置标记
_occupied
: 占位,缓存占用量
通过图我们再来宏观上感受下cache_t
的结构
-
CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
cache_t的结构 -
CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
maskAndBucket时cache_t结构
LLDB调试之
在objc-781基础上定义一个类,编写一些方法:
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end
// LGPerson.m
#import "LGPerson.h"
@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
在main.m中调用它,在[p sayHello];
打下断点,在调试窗口进行调试
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson class];
[p sayHello];
[p sayCode];
[p sayMaster];
按照之前宏观下分析的结构,调试结果如下:
(lldb) p/x pClass // 打印类的指针地址
(Class) $0 = 0x0000000100002298 LGPerson
(lldb) p (cache_t *)0x00000001000022a8 // 采用获取bits类似的方式获取cacche,这里指针移动16位,即加上0x10.
(cache_t *) $1 = 0x00000001000022a8
(lldb) p *$1 // 打印出cache_t的结构
(cache_t) $2 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x000000010032e430 {
_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
}
2020-09-20 00:03:34.346793+0800 KCObjc[8619:340927] LGPerson say : -[LGPerson sayHello]
(lldb) p *$1
(cache_t) $3 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x000000010072c870 {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 11928
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 3
}
_flags = 32804
_occupied = 1
}
这里做了一个操作,当第一次p *$1
时断点停留在 [p sayHello];
,然后断点走到下一步,代码执行了 -[LGPerson sayHello]
操作,再次执行p *$1
,_sel、_imp、_flags 、_occupied
的值发生了变化。初步验证每调用一次方法,就将其缓存。
我们继续调试
(lldb) p $3.buckets() // 打印buckets数组
(bucket_t *) $4 = 0x000000010072c870
(lldb) p *$4
(bucket_t) $5 = {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 11928
}
}
(lldb) p $5.sel() // sel()调用方式是查找cache_t下的定义方法确定的
(SEL) $6 = "sayHello"
(lldb) p $5.imp(pClass) // 同样查看imp是否有相关实现方法:imp(Class cls)
(IMP) $7 = 0x0000000100000c00 (KCObjc`-[LGPerson sayHello]) // 打印出带具体方法实现的函数指针 imp
我们再通过MachOView软件查找可执行文件中的方法以验证正确性。软件具体使用可参考这里
结果显示一致。
脱离源码环境分析
通过源码我们能初探到了 cache_t
中的结构,现在我们再脱离源码环境基础上再来分析下。
新建一个mac工程,创建一个Book
类,属性及方法具体如下:
@interface Book : NSObject
@property (nonatomic,copy) NSString * bookName;
@property (nonatomic,copy) NSString * version;
- (void)read1;
- (void)read2;
- (void)read3;
- (void)read4;
@end
// Book.m
#import "Book.h"
@implementation Book
- (void)read1{
NSLog(@"read book : %s",__func__);
}
- (void)read2{
NSLog(@"read book : %s",__func__);
}
- (void)read3{
NSLog(@"read book : %s",__func__);
}
- (void)read4{
NSLog(@"read book : %s",__func__);
}
@end
在main.m中从源码工程中组建一份与cache_t
结构相似的代码,去除一些非本研究内容:
#import <Foundation/Foundation.h>
#import "Book.h"
#import <objc/runtime.h>
typedef uint32_t mask_t;
struct my_buckt_t { // bucket中存储的就是sel,imp,所以保留两个即可
SEL _sel;
IMP _imp;
};
struct my_cache_t { // 只保留cache_t中重要的研究对象
struct my_buckt_t * _buckets;
mask_t _mask;
uint16_t _flags;
uint16_t _occupied;
};
struct my_class_data_bits_t{ // 简化bits
uintptr_t bits;
};
struct my_objc_class { // 取消继承自objc_object
Class ISA; // 由于未继承了,所以得打开isa注释
Class superclass;
struct my_cache_t cache; // formerly cache pointer and vtable
struct my_class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
Book * book = [Book alloc];
Class bClass = [Book class];
// book.bookName = @"Object-C";
// book.version = @"1.0.0";
[book read1];
// [book read2];
// [book read3];
struct my_objc_class * my_bClass = (__bridge struct my_objc_class *)(bClass);
NSLog(@"occupied:%hu - mask:%u",my_bClass->cache._occupied, my_bClass->cache._mask);
for (mask_t i = 0; i < my_bClass->cache._mask; i ++) {
// 打印获取的bucket
struct my_buckt_t bucket = my_bClass->cache._buckets[I];
NSLog(@"sel:%@ - imp:%p",NSStringFromSelector(bucket._sel),bucket._imp);
}
}
return 0;
}
执行之后会得到下面结果
其中
_occupied = 1, mask = 3
,在打印出的buckets数组中,只有第一个有值。当我们打开read2,read3的注释,结果就会是这样:随着方法调用的增加,mask的值发生了变化。而sel、imp并没有随着调用方法增加,而增加值,依然是有很多的控制。那么
occupied,mask
是什么?底层是怎么做的?接下来我们继续本文重点内容,cache
底层原理。
cache底层原理
我们切换至781源码工程,再找到cache_t结构定义文件,搜索occupied
,我们找到这样的代码,找到了增加occupied的函数:
搜索调用改方法的地方找到了
// 调用之处,自身加1
void cache_t::incrementOccupied()
{
_occupied++;
}
//---
ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
runtimeLock.assertLocked();
#endif
ASSERT(sel != 0 && cls->isInitialized());
// 当缓存少于3/4时,按原样使用
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); // 在原来的容量上完成扩容操作
}
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.计算缓存占用量
mask_t newOccupied = occupied() + 1;
这里是新建一个newOccupied
,这一步是为了计算缓存占用量。这里有个需要注意的点,occupied的值在[p init]
时,occupied = 1;当给属性赋值时会调用set
方法,也会是得occupied的值增加。在脱离源码分析的工程中,我们未调用init、属性,这就解释了我们打印的occupied
的为1。
2.开辟空间
- 1)默认开辟4个单位的缓存空间
- 2)缓存占用量少于 3/4 时,不做操作
- 3)其他情况,在原缓存空间基础上扩容2倍。
if (slowpath(isConstantEmptyCache())) { // 如果未空,执行初始化操作
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE; // INIT_CACHE_SIZE = 1 << 2 = 0100 = 4
reallocate(oldCapacity, capacity, /* freeOld */false); // 初始化 开辟空间
} else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
// Cache is less than 3/4 full. Use it as-is.
}else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 扩容两倍 2 * 4
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true); // 内存库容完毕
}
reallocate
3.缓存bucket - sel & mask
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);
mask_t m = capacity - 1;
就是之前脱离源码环境分析的sel = 3,mask = 7
,未扩容和扩容的两个情况。
mask_t begin = cache_hash(sel, m);
这个是用到哈希算法。如果在之前基础上进行了扩容,那么利用哈希算法就得找到之前存储的位置进行重排存储。计算得到sel
下标,
1).如果第一次进入存储,那么sel的下标是从0开始存储,此时occupied从0开始加1;
2).当第二次进入的存储过程,sel下标如果和即将插入存储的sel相同,则进行原样存储;
3).当再次新存储的sel在遍历bucket存储空间,已经存储,那么就要cache_next
,算法重排,再次计算一个新的下标进行存储。
所以在bucket扩容后,会重新生成,所以会有看起来原来位置的sel为空,其实只是梳理了空间,换了个存储位置,而这个位置是乱序查找插入的,非遍历形式的好处是存储和取出的效率更高。
cache_hash
:对存储进行哈希
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask; // mask = capacity -1 ,sel & mask
}
cache_next
哈希重排
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask; // 1 & 7 = 1, 2 & 7 = 2, 1则就可以作为返回值以供查找下标
}
总结
通过一顿操作探究,我们得到了以下的信息:
1.在cache中,occupied
是一个缓存占用量,用来判断是否需要进行扩容,大于容量的3/4时就必须要扩容;
2.mask
字面意思数面具,是哈希存储过程中的下标。它等于 capacity - 1;
,如果扩容之后是8 ,那么在利用哈希存储的时候,最高就到7,不然就要越界了;
3.在方法调用(包括init)、属性设置过程中,都会引起occupied
的增加;
4.扩容后的cache,在存储bucket时,是利用哈希算法存储的,哈希算法存储会比遍历效率更高。
网友评论