前言
IOS 底层原理之对象的本质&isa关联类 和 IOS底层原理之类结构分析 分别分析了isa
和bits
。类
里面的成员变量还有superclass
和cache
,今天就来探究下cache
的底层原理。说实话内心觉着cache
没什么好探索的不就是个缓存嘛。我承认我飘了,我向cache
道歉,个人觉着cache
的底层探索是比较复杂的,里面有许多苹果底层代码的设计思路
准备工作
- 速效救心丸
- 枸杞茶
- objc4-818.2 源码
cache 结构分析
首先探究下cache
的类型cache_t
,源码中查看cache_t
具体类型发现底层也是结构体
cache_t
结构分析
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
/*
#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
// __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 的掩码
*/
public:
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
unsigned capacity() const;
struct bucket_t *buckets() const;
Class cls() const;
void insert(SEL sel, IMP imp, id receiver);
// 下面是基本上都是其他的方法的方法
};
复制代码
_bucketsAndMaybeMask
变量uintptr_t
占8字节
和isa_t
中的bits
类似,也是一个指针类型里面存放地址联合体
里有一个结构体
和一个结构体指针_originalPreoptCache
结构体
中有三个成员变量_maybeMask
,_flags
,_occupied
。__LP64__
指的是Unix
和Unix
类系统(Linx
和macOS
)_originalPreoptCache
和结构体是互斥
的,_originalPreoptCache
初始时候的缓存,现在探究类中的缓存,这个变量基本不会用到cache_t
提供了公用的方法去获取值,以及根据不同的架构系统去获取mask
和buckets
的掩码
在cache_t
看到了buckets()
,这个类似于class_data_bits_t
里面的提供的methods()
,都是通过方法获取值。查看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
....
//下面是方法省略
};
复制代码
bucket_t
区分真机
和其它
,但是变量没变都是_sel
和_imp
只不过顺序不一样bucket_t
里面存的是_sel
和_imp
,cache
里面缓存的应该是方法
cache_t
整体结构图
lldb
调试验证
首先创建LWPerson
类,自定义一些实例方法,在main
函数中创建LWPerson
的实例化对象,然后进行lldb
调试
cache
的变量的地址,需要首地址
偏移16字节
即0x10
,cache
的地址首地址
+0x10
cache_t
中的方法buckets()
指向的是一块内存的首地址,也是第一个bucket
的地址p/x $3.buckets()[indx]
的方式打印内存中其余的bucket
发现_sel
和imp
LWPerson
对象没有调用对象方法,buckets
中没有缓存方法的数据
在lldb
中调用对象方法,[p sayHello]
继续lldb
调试
- 调用
sayHello
后,_mayMask
和occupied
被赋值,这两个变量应该和缓存是有关系 bucket_t
结构提供了sel()
和imp(nil,pClass)
方法sayhello
方法的sel
和imp
,存在bucket
中,存在cache
中
总结
通过lldb
调试,结合源码。cache
中存的是方法,方法的sel
和imp
存在bucket
。lldb
调试是比较麻烦的比如调用方法后,需要重新获取bukets()
,不舒适,不丝滑。有没有一种比较丝滑的方法呢,那是必须有的嘛
代码转换测试
通过lldb
调试和源码,基本弄清楚cache_t
的结构。我们可以按照cache_t
的代码结构模仿写一套,这样就不需要在源码环境下的通过lldb
。如果需要调用方法,直接添加代码,重新运行就好,这是我们最熟悉的方式了。
typedef uint32_t mask_t;
struct lw_bucket_t {
SEL _sel;
IMP _imp;
};
struct lw_cache_t{
struct lw_bucket_t * _buckets;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct lw_class_data_bits_t{
uintptr_t bits;
};
struct lw_objc_class {
Class ISA;
Class superclass;
struct lw_cache_t cache;
struct lw_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
LWPerson * p = [LWPerson alloc];
[p sayHello1];
[p sayHello2];
//[p sayHello3];
//[p sayHello4];
//[p sayHello5];
Class lwClass = [LWPerson class];
struct lw_objc_class * lw_class = (__bridge struct lw_objc_class *)(lwClass);
NSLog(@" - %hu - %u",lw_class->cache._occupied,lw_class->cache._maybeMask);
for (int i = 0; i < lw_class->cache._maybeMask; i++) {
struct lw_bucket_t bucket =lw_class->cache._buckets[i];
NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
}
return 0;
}
复制代码
2021-06-23 14:51:20.003332+0800 testClass[7899:291790] ---[LWPerson sayHello1]---
2021-06-23 14:51:20.003432+0800 testClass[7899:291790] ---[LWPerson sayHello2]---
2021-06-23 14:51:20.003516+0800 testClass[7899:291790] - 2 - 3
2021-06-23 14:51:20.003603+0800 testClass[7899:291790] sayHello2 - 0x80b0f
2021-06-23 14:51:20.003688+0800 testClass[7899:291790] sayHello1 - 0x8360f
2021-06-23 14:51:20.003778+0800 testClass[7899:291790] (null) - 0x0f
复制代码
objc_class
的Class ISA
是被注释的,因为objc_class
是继承objc_object
,她可以继承objc_object
的Class ISA
,自定义的结构体lw_objc_class
要手动添加Class ISA
,不然代码转换会转换错误- 自己定义的结构体越简便越好,只要能显示出主要的信息就可以
在添加sayHello3
,sayHello4
和sayHello5
方法,看下打印结果
2021-06-23 14:53:45.514704+0800 testClass[7944:294241] ---[LWPerson sayHello1]---
2021-06-23 14:53:45.514817+0800 testClass[7944:294241] ---[LWPerson sayHello2]---
2021-06-23 14:53:45.514899+0800 testClass[7944:294241] ---[LWPerson sayHello3]---
2021-06-23 14:53:45.514982+0800 testClass[7944:294241] ---[LWPerson sayHello4]---
2021-06-23 14:53:45.515069+0800 testClass[7944:294241] ---[LWPerson sayHello5]---
2021-06-23 14:53:45.515161+0800 testClass[7944:294241] - 3 - 7
2021-06-23 14:53:45.515235+0800 testClass[7944:294241] (null) - 0x0f
2021-06-23 14:53:45.515316+0800 testClass[7944:294241] sayHello3 - 0x180b8f
2021-06-23 14:53:45.515411+0800 testClass[7944:294241] (null) - 0x0f
2021-06-23 14:53:45.515525+0800 testClass[7944:294241] sayHello4 - 0x180e8f
2021-06-23 14:53:45.515610+0800 testClass[7944:294241] (null) - 0x0f
2021-06-23 14:53:45.515743+0800 testClass[7944:294241] sayHello5 - 0x180d8f
2021-06-23 14:53:45.515827+0800 testClass[7944:294241] (null) - 0x0f
复制代码
我们会产生下面几个疑问
_occupied
和_maybeMask
是什么?怎么还在变化呢?sayHello1
和sayHello2
方法怎么消失了?是谁施了魔法吗?cache
存储的位置怎么是乱序的呢?比如sayHello2
在sayHello1
前面,sayHello3
前面的位置是空的
带着这些疑问继续探讨cache_t
,下一步怎么走呢?想要知道_occupied
和_maybeMask
是什么?只有去看源码,看看在什么地方赋值的。我们要缓存方法,首先就要是怎么把方法插入到buket
中的。带着这个思路让我们遨游
cache_t
源码中
cache_t
源码探究
首先找到方法缓存
的入口
insert(SEL sel, IMP imp, id receiver)
里面有参数sel
和imp
,这不就是我们熟悉的方法嘛。而且还有方法名insert
,看看它的具体实现,由于insert
内的代码过多我们分步骤说明
计算当前所占容量
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)
开辟newCapacity * bucket_t
大小的内存end->set
将开辟内存的最后一个位置存入sel
=1
,imp
=第一个buket位置的地址
setBucketsAndMask
方法探究
setBucketsAndMask
主要根据不同的架构系统向_bucketsAndMaybeMask
和 _maybeMask
写入数据
collect_free
方法探究
collect_free
主要是清空数据,回收内存
setBucketsAndMask
方法探究
容量不超过3/4
- 当需要缓存的方法所占的容量总容量
3/4
是就会直接走缓存流程 - 苹果的设计思想,探究了很多底层就会发现,苹果做什么事情都会留有余地。一方面可能为了日后的优化或者扩展,另一方面可能是为了安全,内存对齐也是这样
容量存满
- 苹果提供变量,很人性化,如果你需要把缓存的容量存满,默认是不存满的
- 个人建议不要存满,就按照默认的来,如果存满有可能出现其它的问题,很难去排查
容量超过3/4
- 容量超过
3/4
,系统此时会进行两倍扩容
,扩容的最大容量不会超过mask
的最大值2^15
- 扩容的时候会进行一步重要的操作,开辟新的内存,释放回收旧的内存,此时的
freeOld = true
缓存方法
- 首先拿到
bucket()
指向开辟这块内存首地址,也就是第一个bucket
的地址,bucket()
既不是数组也不是链表,只是一块连续的内存 hash
函数根据缓存sel
和mask
,计算出hash
下标。为什么需要mask
呢?mask
的实际作用是告诉系统你只能存前capacity - 1
中的位置,比如capacity = 4
时,缓存的方法只能存前面3
个空位- 开始缓存,当前的位置没有数据,就缓存该方法。如果该位置有方法且和你的方法一样的,说明该方法缓存过了,直接
return
,如果存在hash冲突
,下标一样,sel
不一样,此时会进行再次hash
,冲突解决继续缓存
cache_hash
和 cache_next
cache_hash
主要是生成hash
下标,cache_next
主要是解决hash
冲突
缓存写入方法set
set
把sel
和imp
写入bucket
,开始缓存方法
incrementOccupied
_occupied
自动加1
,_occupied
表示内存中已经存储缓存方法的的个数
cache_t
流程图后面在画
总结
cache_t
中各个变量的含义
_bucketsAndMaybeMask
存取了buckets
和msak
(真机),macOS
或者模拟器存取了buckets
_maybeMask
是指掩码数据,用于在哈希算法或者哈希冲突算法中哈希下标_maybeMask
=capacity -1
_occupied
会随着缓存的个数增加,扩容是_occupied
=0
- 数据丢失是因为
扩容
的时候旧的内存回收了数据全部清除 cache
存储bucket
的位置乱序,因为位置是hash
根据你的sel
和mask
生成所以不固定
总结
探索的过程真的是痛并快乐着