在前面的文章中,我们探索了isa
、superclass
、bits
属性
iOS底层原理07:类 & 类结构分析
iOS底层原理08:类结构分析——bits属性
本文主要探索cache
的结构和底层原理
1、探索cache的数据结构
cache
的类型是cache_t
结构体
1.1、cache_t结构体
来看看objc4-818
源码中cache_t
结构体
typedef unsigned long uintptr_t;
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
struct cache_t {
private:
// explicit_atomic 显示原子性,目的是为了能够 保证 增删改查时 线程的安全性
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字节
};
//下面是一些static属性和方法,并不影响结构体的内存大小,主要是因为static类型的属性 不存在结构体的内存中
/*
#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
// macOS 或 __arm64__的模拟器
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
//__arm64__的真机
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__
//32位 真机
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
//macOS 模拟器
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
****** 中间是不同的架构之间的判断 主要是用来不同类型 mask 和 buckets 的掩码
*/
// ...省略代码
// 下面是几个比较重要的方法
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
unsigned capacity() const;
struct bucket_t *buckets() const;
Class cls() const;
void insert(SEL sel, IMP imp, id receiver);
/// 快速计算对象内存大小,16字节对齐,在对象的alloc中我们已经分析过了
size_t fastInstanceSize(size_t extra) const {...}
// ...省略代码
}
cache_t
是结构体类型,有两个成员变量:_bucketsAndMaybeMask
和一个联合体
-
_bucketsAndMaybeMask
是uintptr_t
类型,占8字节
- 联合体里面有两个成员变量:
结构体
和_originalPreoptCache
,联合体由最大的成员变量的大小决定-
_originalPreoptCache
是preopt_cache_t *
结构体指针,占8字节
- 结构体中有
_maybeMask
、_flags
、_occupied
三个成员变量。-
_maybeMask
的大小取决于mask_t
即uint32_t
,占4字节
-
_flags
是uint16_t
类型,占2字节
-
_occupied
是uint16_t
类型,占2字节
-
-
所以cache_t
的大小等于 8+8
或者8+4+2+2
,即16字节
-
cache_t
结构体提供了buckets()
方法,返回类型是bucket_t *
结构体指针
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
-
cache_t
结构体还提供了insert
方法,插入sel
和imp
,即对方法的缓存
void cache_t::insert(SEL sel, IMP imp, id receiver) {
//对各种不符合条件的判断报出错误码
//省略代码。。。。
//通过buckets数组来判断需要插入的内容情况
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
//省略代码。。。。
}
1.2、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__ // arm64架构
explicit_atomic _imp;
explicit_atomic _sel;
#else // 其他架构
explicit_atomic _sel;
explicit_atomic _imp;
#endif
// ...省略代码
}
-
bucket_t
的成员顺序与架构
有关 -
bucket_t
有两个成员变量_sel
和_imp
,存储方法
的信息
1.3、lldb调试验证
- 创建
HTPerson
类
-
main.m
中代码如下,在[p sayHello];
设置断点,运行代码
- 通过
p *$1
查看cache
的值,此时_maybeMask.Value
和_occupied
的值都为0
- 执行完
[p sayHello];
对象方法,继续查看cache
的值,此时_maybeMask.Value
和_occupied
的值都发生了变化
- 完整的
lldb
调试过程如下图
通过源码
和lldb调试
,可以发现 cache
存储的 方法缓存
- 调用对象方法
sayHello
后,_maybeMask
和_occupied
被赋值,这两个变量应该和缓存是有关系的,我们在后面进行深入分析。 -
bucket_t
结构体提供了sel()
和imp(nil, Class)
方法
2、根据源码,对类和cache进行仿写
为什么需要进行代码仿写
呢?
- 当
源码
无法直接运行调试时,就需要进行代码仿写
; - 使用
lldb
调试时,增减一些属性、方法,就需要再次执行比较多的重复步骤,比较繁琐; - 小规模取样的方式,会让你对底层更加清晰。
2.1、准备工作
- 新建一个
macOS -> Command Line Tool
工程,并创建HTPerson
类,代码如下:
/*** HTPerson.h ***/
#import
NS_ASSUME_NONNULL_BEGIN
@interface HTPerson : NSObject
- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
- (void)say6;
- (void)say7;
+ (void)sayHappy;
@end
NS_ASSUME_NONNULL_END
/*** HTPerson.m ***/
#import "HTPerson.h"
@implementation HTPerson
- (void)say1{
NSLog(@"%s",__func__);
}
- (void)say2{
NSLog(@"%s",__func__);
}
- (void)say3{
NSLog(@"%s",__func__);
}
- (void)say4{
NSLog(@"%s",__func__);
}
- (void)say5{
NSLog(@"%s",__func__);
}
- (void)say6{
NSLog(@"%s",__func__);
}
- (void)say7{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"%s",__func__);
}
@end
-
main.m
文件中代码如下:
#import
#import "HTPerson.h"
typedef uint32_t mask_t;
// bucket_t结构体
struct ht_bucket_t {
SEL _sel;
IMP _imp;
};
// cache_t结构体
struct ht_cache_t {
struct ht_bucket_t * _buckets; //8字节
mask_t _maybeMask; //4字节
uint16_t _flags; //2字节
uint16_t _occupied;//2字节
};
// 类结构体
struct ht_objc_class {
Class isa; // 8字节
Class superclass; //8字节
struct ht_cache_t cache; //16字节
uintptr_t bits; // 8字节
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson *p = [HTPerson alloc];
Class pClass = p.class;
// [p say1];
// [p say2];
// [p say3];
// [p say4];
// [p say5];
// [p say6];
// [p say1];
// [p say2];
struct ht_objc_class * ht_class = (__bridge struct ht_objc_class *)(pClass);
NSLog(@"- %hu - %u", ht_class->cache._occupied, ht_class->cache._maybeMask);
for (int i = 0; i < ht_class->cache._maybeMask; i++) {
struct ht_bucket_t bucket = ht_class->cache._buckets[I];
NSLog(@"%@ - %pf", NSStringFromSelector(bucket._sel), bucket._imp);
}
}
return 0;
}
2.2、对象方法的调用 与 cache值的关系
- 未调用对象方法
如果对象方法
都没有调用,则cache
不会进行方法缓存,此时_occupied
和_maybeMask
的值都为0
- 调用
say1
和say2
方法,查看打印结果
- 如果继续调用
say3
、say4
和say5
方法呢
【问题】 这里就产生了几个疑问?
-
_occupied
和_maybeMask
是什么?他们的值是如何变化? - 调用
say3
、say4
和say5
方法后,say1
和say2
怎么消失了? -
cache
存储的位置怎么是乱序的呢?
_occupied
和_maybeMask
是什么?在什么地方赋值,只能去objc
源码中找答案。我们要缓存方法
,首先看怎么把方法插入到bukets
中的。带着这些疑问继续探讨cache_t
源码
3、cache_t源码探究
- 首先找到
方法缓存
的入口
从这个插入的方法来看,插入的参数有sel
、imp
、还有receiver
消息接收者。下面是这个插入方法的代码实现:
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;
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 <= 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);
}
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.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
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
}
计算当前所占容量
-
occupied()
获取当前所占的容量,其实就是告诉你缓存中已经有几个bucket
-
newOccupied = occupied() + 1
,表示当前方法第几个进来缓存的 -
oldCapacity
目的是为了重新扩容的时候释放旧的内存
开辟容量
- 第一次缓存方法的时,开辟默认容量是
capacity = INIT_CACHE_SIZE
即capacity = 4
就是4
个bucket
的内存大小 -
reallocate(oldCapacity, capacity, /* freeOld */false)
开辟内存,freeOld
变量控制是否释放旧的内存
reallocate方法探究
reallocate
方法主要做三件事
-
allocateBuckets
开辟内存 -
setBucketsAndMask
设置mask
和buckets
的值 -
collect_free
是否释放旧的内存,由freeOld
控制
allocateBuckets方法(开辟内存)探究
allocateBuckets
方法主要做两件事
-
calloc(bytesForCapacity(newCapacity), 1)
开辟内存 -
end->set
将开辟内存的最后一个位置存入sel = 1
,imp = 第一个buket位置的地址
setBucketsAndMask 方法探究
setBucketsAndMask
方法主要用来赋值
- 根据不同的架构系统向
_bucketsAndMaybeMask
和_maybeMask
写入数据 -
_occupied
重置为0
collect_free 方法探究
-
collect_free
方法主要是清空数据
,回收内存
二倍扩容
- 当
方法缓存
到总容量的3/4
或者7/8
时,回进行二倍扩容
-
二倍扩容
即开辟2倍
新内存,释放旧内存
方法缓存
- 首先拿到
buckets()
,即开辟这块内存首地址,也就是第一个bucket
的地址,buckets()
既不是数组也不是链表,只是一块连续的内存 -
cache_hash
方法计算hash下标
,cache_next
方法处理hash冲突
- 如果当前的位置没有数据,就缓存该方法;如果该位置有方法且和你的方法一样的,说明该方法缓存过了,直接
return
;如果存在hash冲突
,下标一样,sel不一样,此时会进行再次hash
,冲突解决继续缓存
方法缓存写入方法 set
set
方法:将imp
和sel
写入bucket
中
insert方法调用流程
前面探究了insert方法
的源码实现,接下来我们探究insert方法调用流程
,是如何从调用实例方法
走到cache里面的insert方法
的?
- 首先在
insert方法
中打个断点,然后运行源码
,查看函数调用栈
从堆栈信息可以看出insert
的调用流程:_objc_msgSend_uncached
--> lookUpImpOrForward
--> log_and_fill_cache
--> cache_t::insert
【问题】 _objc_msgSend_uncached
方法又是何时调用的呢?
- 在
objc4-818源码
中搜索_objc_msgSend_uncached
如下图
我们发现:objc_msgSend
方法会调用_objc_msgSend_uncached
,至此整个流程就串联起来了
- 方法调用的本质就是
消息发送
,即调用objc_msgSend
- 方法缓存的调用流程:
objc_msgSend
-->_objc_msgSend_uncached
-->lookUpImpOrForward
-->log_and_fill_cache
-->cache_t::insert