类设计原理
类的本质和基本结构
- 在开发过程中,很多类都要去继承
NSObject
。而类的本质是一个含有isa
成员的objc_object
结构体。isa
是一个指向objc_class
的指针。所以,objc_class
的设计就很重要。
struct objc_class : objc_object {
//Class ISA; //指向元类的指针
Class superclass; //超类
cache_t cache; //缓存使用过的方法等信息,在下次使用时可以快速查找到,节约查找成本
class_data_bits_t bits; //用来查找存储在内存中的方法、属性等
...
}
复制代码
结构体中各成员的存在意义
isa
:指向类或者元类的指针。一个类型的类或元类在内存中只存在一个。一个类对应多个实例对象,对象执行某个方法,需要根据该isa去类和元类中查询。superclass
:用来获取父类的信息。在需要的时候会去这里查询方法地址,属性地址等。cache
:存储一些使用过的方法等信息(比如父类的方法)。避免多次查询消耗资源。bits
:本类的方法,属性等存储的地址。
cache在类中扮演的角色
- 对象方法的执行,都是以信号进行传递的(objc_msgSend),执行过程中会去内存中找该对象的方法实现内存地址。然后才可以执行。
- cache的存在意义就是把使用过的方法地址缓存起来。减少查找内存的长度以及复杂度。
cache的源码探究
cache_t的结构
cache
是一个cache_t
类型的结构体,查看源码如下所示。
struct cache_t {
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8个内存
union {
struct {
//当前的缓存区count,第一次开辟是3
explicit_atomic<mask_t> _maybeMask; // 4
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
};
...
struct bucket_t *buckets() const; //查
void insert(SEL sel, IMP imp, id receiver); //插入缓存的方法
...
复制代码
_bucketsAndMaybeMask
:当前存放的是buckets
和maybeMask
_maybeMask
:当前cache
的可存储的buckets
数量,默认是0
_originalPreoptCache
:执行_occupied++,_occupied默认是0,每次有方法的插入都会被执行,本质上就是占位+1buckets()
:可以获取到缓存中的方法列表insert
:插入新的方法到缓存中
cache_t 实现缓存的过程
insert方法的实现
- 首次走类的方法时没有旧的缓存。默认创建4个容量的存储空间。
- 缓存时会判断缓存的容量是否达到了
3/4
。如果达到了会新创建一个扩容后的内存地址去存储新的方法。并且清除旧的缓存。(为什么要清空oldBuckets,而不是空间扩容,然后在后面附加新的缓存呢?因为已经创建的内存无法更改,即使把旧的缓存取出来,有涉及到了遍历等操作,不如直接新创建一个内存空间,删除旧的缓存。)
- 如果
bucket
为空的,就把sel
和imp
写入到该bucket
中
LLDB输出验证
需要验证的代码
@interface HTPerson : NSObject
- (void)playGame_wangZhe;
- (void)playGame_chiJi;
- (void)playGame_CF;
- (void)playGame_CC;
- (void)playGame_Sleep;
@end
@implementation HTPerson
- (void)playGame_wangZhe{
NSLog(@"%s",__func__);
}
- (void)playGame_chiJi{
NSLog(@"%s",__func__);
}
- (void)playGame_CF{
NSLog(@"%s",__func__);
}
- (void)playGame_CC{
NSLog(@"%s",__func__);
}
- (void)playGame_Sleep{
NSLog(@"%s",__func__);
}
@end
复制代码
LLDB输出结果
在方法执行前,缓存里没有内容。
执行了2个方法后,缓存里生成了4个方法的缓存(2个系统方法)
输出方法名称如下所示
防源代码查看方法缓存
模仿源代码,进行强制转换,来输出bucket。
typedef uint32_t mask_t;
//bucketsMask:掩码,用来通过_bucketsAndMaybeMask解析初buckets
static uintptr_t bucketsMask = ~0ul;
//bucket_t源码模仿
struct HTBucket_t {
SEL _sel;
IMP _imp;
};
struct HTCache_t {
uintptr_t _bucketsAndMaybeMask; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
struct HTClass_data_bits_t {
uintptr_t bits;
};
struct HTObjc_object {
Class isa;
};
struct HTObjc_Class: HTObjc_object{
Class superclass;
HTCache_t cache; // formerly cache pointer and vtable
HTClass_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson *p = [HTPerson alloc];
[p playGame_CC];
[p playGame_Sleep];
//
// [p playGame_wangZhe];
//
// [p playGame_chiJi];
//
// [p playGame_CF];
struct HTObjc_Class *pClass = (HTObjc_Class *)HTPerson.class;
//打印当前有多少个方法缓存与最大缓存数量
NSLog(@"%u-%u",pClass->cache._occupied,pClass->cache._maybeMask);
//通过_bucketsAndMaybeMask解析初buckets
uintptr_t bucketAndM = pClass->cache._bucketsAndMaybeMask;
HTBucket_t *bucket = (HTBucket_t *)(bucketAndM & bucketsMask);
//循环遍历打印缓存的sel与imp
for (int i = 0; i < pClass->cache._maybeMask ; i++) {
struct HTBucket_t b = *(bucket + i);
NSLog(@"第%d位==%@-%p",i,NSStringFromSelector(b._sel),b._imp);
}
}
return 0;
}
复制代码
输出结果如下
_occupied==2个方法-最大缓存数_maybeMask==3
第0位==playGame_CC-0x7f38
第1位==playGame_Sleep-0x7ee8
第2位==(null)-0x0
复制代码
执行3个方法的话,会删除旧的缓存然后创建一个可以存放7个方法的内存
_occupied==1个方法-最大缓存数_maybeMask==7
第0位==(null)-0x0
第1位==(null)-0x0
第2位==(null)-0x0
第3位==playGame_wangZhe-0x7fb0
第4位==(null)-0x0
第5位==(null)-0x0
第6位==(null)-0x0
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END