整体分析类在底层的结构,以及对类的操作在底层是如何实现的,重点包括cache和bits的分析。
主要内容:
- 类的底层结构objc_class的认识,及objc_class与objc_object的关系
- isa的认识,isa的走向以及类的继承关系
- cache的详细解读
- bits的详细解读
1、类的底层结构objc_class的认识
先查找类的底层结构是什么,可以通过Clang编译查看底层得知是通过objc_class来创建的。同时分析objc_class和objc_object的关系。
1.1 创建一个WYPerson类
@interface WYPerson : NSObject
@property (nonatomic, assign) int age;
@end
@implementation WYPerson
- (void)eat{
NSLog(@"eat");
}
1.2 底层结构体
首先看一下Class结构体是什么
在objc源码中可以看到Class就是通过objc_class结构体定义的,它就表示一个类
typedef struct objc_class *Class;
NSObject的结构体
代码:
#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif
struct NSObject_IMPL {
Class isa;
};
说明:
- 通过objc_object结构体定义了一个NSObject对象的结构体
- 而NSObject_IMPL是通过伪继承来实现的,而Class是通过objc_class结构体定义的,所以NSObject_IMPL是基于objc_class来创建的
WYPerson的结构体
代码:
#ifndef _REWRITER_typedef_WYPerson
#define _REWRITER_typedef_WYPerson
typedef struct objc_object WYPerson;
typedef struct {} _objc_exc_WYPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_WYPerson$_age;
struct WYPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};
说明:
- 通过objc_objcet定义了一个WYPerson的对象结构体
- WYPerson_IMPL也是通过伪继承实现的,这里继承自NSObject_IMPL,也就是带有了NSObject的所有成员
1.3 结构体模板
objc_object的认识:
源码:
//可以看到,objc_object中就是一个isa
struct objc_object {
private:
isa_t isa;
public://外部可以获取isa
// getIsa() allows this to be a tagged pointer object
Class getIsa();
}
其实objc_object中还有很多方法,我这里只写了getIsa(),更多的方法可以查看objc源码
说明:
- objc_object只有一个成员,就是isa,其他的都没有
- 那么我们创建一个对象,那么多属性、方法等等在哪里呢,其实都在类里。
- 对象通过isa获取到类信息,之后就可以在类信息中获取相应的属性、成员变量、方法、协议了。
- 此处需要注意的是通过getIsa()获取到的isa并不是完整的,而只有Class信息。
objc_class的认识:
源码:
struct objc_class : objc_object {//继承自objc_object,充分证明万物皆对象
// Class ISA; //来自继承的objc_object
Class superclass; //父类
cache_t cache; // 方法缓存区,快速查询方法
class_data_bits_t bits; // 类信息,方法、协议、属性、成员变量列表
class_rw_t *data() const {//获取bits的数据
return bits.data();
}
}
说明:
- objc_class继承自objc_object,充分说明类也是一个对象,万物起源于objc_object
- 类的结构包括isa,superclass、cache、bits。
- isa的继承自objc_object获取的
- cache用来存储缓存的方法信息,包含sel和imp,可以快速的进行消息查找
- bits的data()包含了方法、协议。属性、成员变量。
1.4 总结
- objc_class是继承自objc_object,因此类本身也是一个类对象,类是元类的对象
- objc_object作为对象的底层结构体有一个isa,说明对象的底层结构只包括isa。
- 所有的对象、类、元类、协议都有isa成员,充分说明在面向对象的世界里万事万物皆对象
- 所有的对象底层是以objc_object为模板创建的结构体,所有的类的底层是以objc_class为模板创建的结构体
- 元类也有isa,因为他也有自己的类就是根元类,根元类的类是他自己,所以根元类也有自己的isa。
- objc_class中包含isa、superClass、cache、bits。
2、isa的认识,isa的走向以及类的继承关系
isa是从objc_object继承过来的,在OC对象的底层分析中已有详细解读,这里不再分析。只了解isa的走向和类的继承关系
通过这个关系图可以形象的了解对象、类、元类、根元类之间关系。
关系图:
2.1 元类
2.1.1 什么是元类
- 元类是类对象的类,每个类都有一个独一无二的元类用来存储类的相关信息。
- 元类的定义和创建的都是系统完成的,是根据我们自定义的类来创建的
- 元类本身没有名称,由于与类关联,名称和类名一样,并且我们无法使用,无法直接看到
- 元类用来存储类信息,所有的类方法都存储在元类中
2.1.2 为什么需要元类(元类的作用是什么)
- 元类用来管理类本身的信息,比如类方法就存放在元类中
- 在面向对象的世界中,万物皆对象,类也是一个对象,叫类对象,而这个类对象所属的类就是元类,通过元类就可以将类作为对象。
- 元类所属的是根元类。
2.1.3 类方法存储在哪里?
- 在上层区分实例方法、类方法,但是在底层都是函数,并无法区分实例方法和类方法,因此可以通过存放的地方不同来区分。
- 类方法存储在元类中,实例方法存放在类中
- 类方法也是以对象方法的姿态存储在元类中
2.2 isa走位分析
2.2.1 根据图示的分析
根据图可以看出:
- 对象的isa指向类
- 类的isa指向元类
- 元类的isa指向根元类
- NSObject类也指向根元类
- 根元类指向根元类自己
2.2.2 验证
在我的前一篇博客对象的底层结构分析中得知isa中包含有类信息,所以我们可以通过查看isa中的类信息来验证走位图中的结果。
1、获取到类信息(对象的isa)
说明:
- x/4gx perosn 可以得到包括isa在内的person属性
- p/x isa值 & ISA_MASK 通过mask取出isa中的类地址
- x/4gx 类地址 得到类的信息
2、查看元类信息(类的isa)
说明:
- x/4gx 类信息地址 得到包括isa在内的类信息
- p/x isa值 & ISA_MASK 得到根元类地址
- 可以看到类信息的isa本身就是类信息的地址,没有任何其他信息
- 因此此时得到的类信息的isa地址其实是LGPerson的元类信息
3、查看根元类(元类的isa)
说明:
- 查看类的isa,发现是NSObject,其实此时是根元类,
4、验证根元类的isa
说明:
- NSObject根元类的isa指向的还是自己
- 从中还可以看出来,类的isa直接就是元类信息,没有其他信息
2.3 继承关系分析
- 继承关系不包括对象,只是类、元类之间的关系
- 所有的类都继承自NSObject(NSProxy除外,关于它的认识请查看博客。。。)
- 元类也有自己的继承链,最终继承自NSObject类
- 重点在于NSObject元类继承自NSObject类。同时NSObject类继承自nil,也就是不继承任何类,这说明NSObject类是万物起源。
- 类的继承链中只有类,元类的继承链中不全是元类,还有一个NSObject类,在元类继承链进行循环查找时要注意这一条。
2.4 总结
- 对象的isa指向类,类指向元类,元类指向根元类,根元类指向自己
- NSObject类指向NSObject根元类,根元类指向自己
- 继承关系指的是类或者元类的关系,而不是对象的关系
- 对象继承于类,元类也有继承关系
- 一定要注意根元类继承于NSObject
- 最后的NSObject继承于nil,NSObject是万物起源
3、 cache
cache是类结构体中的一个属性,cache用来存储已经被调用过的方法的sel和imp。这样可以使方法的调用提高效率,主要研究的内容是cache的结构和存储方式,至于如何在cache中查询imp,会在消息发送过程中详细解读。
3.1 cache的整体结构认识
cache是类结构中用来存储已被调用的方法的sel和imp键值对的一段缓存区,这里就简单看下cache的结构是怎样的,都包含了哪些内容。
3.1.1 cache_t结构体
源码:
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED//macOS、模拟器 -- 主要是架构区分
// explicit_atomic 显示原子性,目的是为了能够 保证 增删改查时 线程的安全性
//等价于 struct bucket_t * _buckets;
//_buckets 中放的是 sel imp
//_buckets的读取 有提供相应名称的方法 buckets()
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //64位真机
explicit_atomic<uintptr_t> _maskAndBuckets;//写在一起的目的是为了优化
mask_t _mask_unused;
//以下都是掩码,即面具 -- 类似于isa的掩码,即位域
// 掩码省略....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非64位 真机
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
//以下都是掩码,即面具 -- 类似于isa的掩码,即位域
// 掩码省略....
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
//方法省略.....
}
在源码中查看,有三种类型,真机64位,真机32位,模拟器或macos,我们研究的是64位真机,
成员:
1)CACHE_MASK_STORAGE_OUTLINED表示非真机,有属性_buckets和_mask
2)CACHE_MASK_STORAGE_HIGH_16表示真机64位,有属性_maskAndBuckets和_mask_unused
3)CACHE_MASK_STORAGE_LOW_4表示真机32位,有属性_maskAndBuckets和_mask_unused
4)uint16_t _flags;
5)uint16_t _occupied;
宏定义架构的判断
#if defined(__arm64__) && __LP64__//真机64位
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__//真机32位
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED//非真机
#endif
- 这里的__arm64是判断真机,__LP64是用来判断64位,所以第一个宏定义是64位真机,第二个宏定义是32位真机,第三个宏定义是模拟器和macOS
- 架构判断,macos是i386,模拟器是x86,真机是arm64。
泛型的查看
属性中使用到了explicit_atomic<uintptr_t> ,只是一个泛型,表示传入一个类型,而这个explicit_atomic的作用是将这个类型做原子操作。
真机64位的结构查看
包含_maskAndBuckets/_flags/_occupied,其中_maskAndBuckets由_mask和buckets组成,这样做是为了优化性能,而在buckets中就存储有sel和imp。
- 因此cache_t结构体在64位真机下包含有_maskAndBuckets、_flags、_occupied
- _maskAndBuckets,由_mask和buckets组成,这样做是为了优化性能,而在buckets中就存储有sel和imp。mask就等于capacity-1,也就是说mask就是当前缓存量-1
- _flags:_flags属性是一些位置标记
- _occupied:sel和imp键值对的数量
3.1.2 bucket_t结构体
这里可以看到存储的就是imp和sel,只是真机和模拟器macOS存储的顺序不一样.
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
}
3.1.3 总结
3.2 脱离源码环境查看cache的数据变化
通过在上层对cache结构进行使用,查看里面的成员是如何进行操作的
本质就是自己搭建一个源码环境,源码底层本来就是结构体,我们在上层也是用结构体来存储和取用就可以实现。
代码:
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct lg_bucket_t {
SEL _sel;
IMP _imp;
};
struct lg_cache_t {
struct lg_bucket_t * _buckets;
mask_t _mask;
uint16_t _flags;
uint16_t _occupied;
};
struct lg_class_data_bits_t {
uintptr_t bits;
};
struct lg_objc_class {
Class ISA;
Class superclass;
struct lg_cache_t cache; // formerly cache pointer and vtable
struct lg_class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson class]; // objc_clas
[p say1];
[p say2];
//[p say3];
//[p say4];
struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
NSLog(@"%hu - %u",lg_pClass->cache._occupied,lg_pClass->cache._mask);
for (mask_t i = 0; i<lg_pClass->cache._mask; i++) {
// 打印获取的 bucket
struct lg_bucket_t bucket = lg_pClass->cache._buckets[i];
NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
}
NSLog(@"Hello, World!");
}
return 0;
}
打印结果:
疑问:
- _mask是什么?
- _occupied 是什么?
- 为什么随着方法调用的增多,其打印的occupied 和 mask会变化?
- 为什么随着方法调用的增多,其打印的occupied 和 mask会变化?
- bucket数据为什么会有丢失的情况?,例如2-7中,只有say3、say4方法有函数指针
- 2-7中say3、say4的打印顺序为什么是say4先打印,say3后打印,且还是挨着的,即顺序有问题?
- 打印的cache_t中的_ocupied为什么是从2开始?
3.3 cache的原理分析
在执行方法后查看cache_t中的属性值,发现了一些疑惑点,这些需要理解底层原理后才可以解释,通过数值的变化occupied开始入手,查找变化的过程。
3.3.1 查找存储的函数
3.3.1.1 在cache_t中查找到设置occupied属性的函数incrementOccupied()
- 我们在实际使用时发现当调用一次方法时,occupied会自增,那么就需要在源码中查看什么情况下会出现自增。
- 通过occupied++,以及occupied+1都没有搜索到,所以就思考是否对外提供方法来进行操作的。查找到incrementOccupied()函数
3.3.1.2 查找到该函数的实现为自增
- 这里可以说明occupied的增加是通过调用incrementOccupied()函数实现的,所以接下来就是看谁在调用该函数
3.3.1.3 全局搜索incrementOccupied()发现在cache_t的insert函数有调用
3.3.1.4 全局搜索insert()方法,发现只有cache_fill方法中的调用符合
3.3.1.5 全局搜索cache_fill,发现在写入之前,还有一步操作,即cache读取,即查找sel-imp,如下所示
- 在这里并没有找到如何去调用的,可能是系统没有给我们展示
- 但是系统说明了它内部执行的流程,是先读缓存,之后再写缓存
3.3.1.6 总结:
- 在发送消息后,缓存需要先被读取
- 如果缓存中不存在,则需要将得到的sel和imp写入缓存
- 写入缓存通过调用cache_fill()->insert()->来实现
3.3.2 insert()函数分析
insert()函数用来插入sel和imp,重点在于哈希算法
源码:
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());
// Use the cache as-is if it is less than 3/4 full只有当缓存的使用小于等于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;//初始容量设置为4
reallocate(oldCapacity, capacity, /* freeOld */false);//开启一个容量为4的空间,并将旧空间删掉
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {//occupied() + 1 + 1 <=3/4,也就是总数+2小于等于总容量的3/4就可以直接存入
// Cache is less than 3/4 full. Use it as-is.
//当缓存的使用量小于等于3/4的时候直接使用缓存
}
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;//扩容两倍
if (capacity > MAX_CACHE_SIZE) {//最大缓存值判断,不能超过2^16,不能无限扩大
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);//开辟空间,也需要删除旧空间
}
bucket_t *b = buckets();//取出第一个bucket,为了下面的循环遍历
mask_t m = capacity - 1;//这里可以看到mask就是capacity-1
mask_t begin = cache_hash(sel, m);//哈希算法,(mask_t)(uintptr_t)sel & mask
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.
//先查询该位置是否为空,如果不为空且存储的内容是其他sel,说明发生了冲突,就通过哈希冲突算法进行计算下标,并再次判断该坐标是否为空
do {
//如果该下标所在的位置是空的,就直接存储
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();//occupied++,每次插入都要给occupied++,因此他记录的是缓存的方法的个数
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));//哈希冲突算法:i ? i-1 : mask;
cache_t::bad_cache(receiver, (SEL)sel, cls);
}
【第一步】:开辟空间
- 得到缓存量的占用量。 这是一个临时变量,假如添加成功之后的一个占用量,用作后面是否进行扩容的判断。
- 得到当前总的缓存量。 这是当前已有的缓存量,主要用来判断这一次是否还需要进行扩容。
- 首次开辟空间。
- 判断容量为空,则进行初始化,首次开辟空间
- 先获取到开辟空间的容量
- 首次的空间是4
- 之后调用reallocate()函数进行开辟
- 传入的是false,不会删除旧空间,因为是首次开辟
- 直接存储,不需要扩容
- 当缓存的使用小于等于3/4的时候直接使用缓存
- 需要注意的是,在当前缓存的基础上再前面先+1,在这个判断里又进行了+1,也就是当前缓存+2后小于等于当前容量的3/4才不需要扩容
- 这是因为要需要考虑多线程的影响
- 扩容
- 一次扩容是在之前容量的基础上增加一倍的容量
- 容量有最大值,是2的16次方
- 传入的值是True,所以需要删除旧空间,开辟新空间,扩容后需要再次开辟空间
【第二步】:计算存储位置
通过哈希算法来计算存储位置,哈希算法肯定会涉及哈希冲突,哈希冲突后需要再次进行哈希冲突计算来得到存储位置。具体的哈希冲突算法后面再分析。
代码:
bucket_t *b = buckets();//取出第一个bucket,为了下面的循环遍历
mask_t m = capacity - 1;//这里可以看到mask就是capacity-1
mask_t begin = cache_hash(sel, m);//哈希算法,(mask_t)(uintptr_t)sel & mask
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.
//先查询该位置是否为空,如果不为空且存储的内容是其他sel,说明发生了冲突,就通过哈希冲突算法进行计算下标,并再次判断该坐标是否为空
do {
//如果该下标所在的位置是空的,就直接存储
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();//occupied++,每次插入都要给occupied++,因此他记录的是缓存的方法的个数
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));//哈希冲突算法:i ? i-1 : mask;
代码说明:
- 通过cache_hash()算法来得到存储下标
- 如果该存储位置的sel()为空,则直接存储
- 如果该存储位置的sel()与需要插入的sel一致,则不存储,直接使用
- 否则说明已经冲突了,就使用cache_next()哈希冲突算法进行计算存储位置,重新进行比较
【第三步】:存储
存储就是直接设置即可,存入的有sel、imp、cls。
代码:
cache_t::bad_cache(receiver, (SEL)sel, cls);
3.3.3 reallocate()函数分析
该方法用来开辟新空间,重点在于如果是扩容,需要删除旧空间。
源码:
说明:
- 通过新的容量来创建新的bucket_t的空间
- 通过setBucketsAndMask()函数将新的buckets和capacity设置到cache中
- 通过cache_collect_free()函数删除旧的buckets
3.3.4 setBucketsAndMask()函数分析
该方法用来设置buckets和mask,有三种,真机64位、真机非64位,非真机。
3.3.5 cache_collect_free()函数分析
该方法对数据进行垃圾回收
- 垃圾回收,清理旧的bucket
- 扩容后不需要移植缓存信息,这样可以提高性能,因为当调用的方法越多,记录的会越多,这样每次扩容移植的信息就越多,性能会差,同时还可以避免一定程度的哈希冲突。
3.3.6 _garbage_make_room函数
- 创建垃圾回收空间
- 如果是第一次,需要分配回收空间
- 如果不是第一次,则将内存段翻倍,即原有内存*2
3.3.7 疑问解答
经过对原理的分析,接下来就可以对前面的疑问进行解答:
问:_mask是什么?
答:_mask是指掩码数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask 等于capacity - 1。
问:_occupied 是什么?
答:已经存储了sel-imp的的个数,只要有新的方法调用,需要在cache中存储时,就会给_occupied+1。
问:为什么随着方法调用的增多,其打印的occupied 和 mask会变化?
答:随着新方法调用,自然会将方法存储到cache中,那么_occupied就会自增。
而存储到cache中的sel-imp不断增加,需要对cache扩容,而mask就是容量-1,所以mask也会增加。
问:bucket数据为什么会有丢失的情况?,例如2-7中,只有say3、say4方法有函数指针
答:在扩容时,会将之前创建的内存空间删除,重新创建新空间,而不是在之前的基础上增加空间,因此扩容后之前已经存储到cache的sel-imp并不会存在了。
问:2-7中say3、say4的打印顺序为什么是say4先打印,say3后打印,且还是挨着的,即顺序有问题?
答:存储方式是哈希表,通过哈希算法计算下标,并不是顺序存储结构,
问:打印的cache_t中的_ocupied为什么是从2开始?
答:这里是因为LGPerson通过alloc创建的对象,并对其两个属性赋值的原因,属性赋值,会隐式调用set方法,set方法的调用也会导致occupied变化
3.3.8 总结
- cache存储了sel-imp,存储在哈希表中,通过哈希算法来计算下标
- 首次创建的容量大小是4
- 当存储的occupied+2大于容量的3/4时就会扩容
- 扩容并不是在原来内存的基础上扩展内存,而是删除旧空间,创建新空间
- 一次扩容是原来容量的2倍,容量最多是2的16次方
3.4 掩码计算
主要介绍maskAndBuckets的存储格式,以及如何通过掩码进行计算分别获取maskAndBuckets、mask、buckets。
3.4.1 存储格式:
- maskAndBuckets总共64位
- mask占有高16位
- buckets占有低44位
- 中间4位对于消息发送会更有优势,在查询缓存时会用到(这里先记一下,后面在消息发送的分析中会用到)
3.4.2 掩码数据
- maskShift用来计算mask的,将maskAndBuckets向右平移maskShift个位数就得到了mask
- maskZeroBits是存储中间4位的,拿它可以用来计算buckets的位数,以此可以得到buckets的面具
- maxMask表示最大的mask
- mask = 容量-1
- 最大容量就是2^16,所以mask = 2^16-1
- mask就占有16位
- bucketsMask就是buckets的掩码,是一个后44位全为1的数。
- 计算方式为1<<(maskShift-maskZeroBits) -1
- maskShift-maskZeroBits = 48-4 = 44
- 1<<44得到的数据是在第45位为1,后44为0
- 1<<44-1得到的数据就是后44位都为1了
3.4.3 掩码计算:
buckAndBuckets的计算:
- 传入的值newBuckets就是新开辟的buckets
- 传入的newMask就是mask
- __mindmap__topic先将mask向左平移48位,放置到高16位
- 之后再和buckets或一下,就将buckets放置到后44位了
- 这里应该用|,也就是只要有1,就显示为1
- 这样就可以把前16位保存下来,后44位也保存下来
buckets:
- bucketsMask 1<<44 -1
- 1<<44 。 在第45位为1,后44为0
- 1<<44 -1。 减去1,就是后44位都为1了
- 所以跟bucketsMask相与就得到后44位的buckets
mask:
- 向右平移48位,也就是向前16位移到最后,得到mask
3.5 哈希算法
这里要着重注意,因为在快速消息查找时,通过传入的sel在cache中查询imp,就需要通过哈希算法。快速查找是通过汇编来实现的,因此如果这里不清楚,对于后面的查找过程就更难以理解了。
3.5.1 哈希简单认识
如何进行地址存储
bucket存储在哈希表中,所以需要通过哈希算法来存储,将存储的对象作为变量,通过执行一个哈希算法的哈希函数进行计算得到存储地址。
哈希冲突
如果两个存储对象经过哈希算法计算的到的哈希地址是同一个地址,则就表示出现了哈希冲突,而要解决就需要将该变量用一个哈希冲突算法计算得到新的哈希地址。如果仍然冲突,则继续用哈希冲突函数计算得到新的哈希地址
3.5.2 存储逻辑:
源码:
bucket_t *b = buckets();//取出第一个bucket,为了下面的循环遍历
mask_t m = capacity - 1;//这里可以看到mask就是capacity-1
mask_t begin = cache_hash(sel, m);//哈希算法,(mask_t)(uintptr_t)sel & mask
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.
//先查询该位置是否为空,如果不为空且存储的内容是其他sel,说明发生了冲突,就通过哈希冲突算法进行计算下标,并再次判断该坐标是否为空
do {
//如果该下标所在的位置是空的,就直接存储
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();//occupied++,每次插入都要给occupied++,因此他记录的是缓存的方法的个数
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));//哈希冲突算法:i ? i-1 : mask;
1、先通过哈希算法得到哈希地址
2、判断如果改地址没有存储sel,则直接存入
3、如果已经存入,但存入的就是想要存入的sel,则直接返回(因为多线程)
4、如果已经存入且是其他的sel,则说明冲突
5、冲突后进行哈希冲突算法计算的到哈希地址,
6、如果哈希地址与最早的哈希地址不一致,则进入执行
7、如果相等,则直接退出,因为哈希冲突地址使用了上一轮的i来进行计算
* 如果相等了,下次计算仍然相等,所以相等之后就停止不再计算,否则会产生死循环
* 这种情况下也就是哈希冲突算法把所有的位置都遍历了一遍,所以就直接退出。
3.5.3 哈希算法:
源码:
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
- 这里也可以看到sel与mask相与得到哈希地址
- 而这个mask就是容量-1
3.5.4 哈希冲突算法:
源码:
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;//将下标+1,再与上mask
}
//arm64,我们看这个
#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;//判断如果i存在,将下标-1,也就是向前一位存储,如果为0,也就是计算到第一个位置,就直接放到mask,也就是最后一个位置
}
- 这里可以看到其实哈希冲突算法很简单,只是向前移动一位
- 一直移动到第一位仍然冲突,就放到最后一位。之后继续向前移动
- 一直移动到最初哈希算法得到的值时就会退出循环
总结:
1、先通过哈希算法得到哈希地址
2、判断如果已经存在一个值但是不是当前需要存入的值,就说明哈希冲突了,就执行哈希冲突算法
3、如果哈希地址中没有sel,就直接存入
3、如果哈希地址有sel,并且就是这次需要存入的值,说明其他线程已经存入了,就直接返回即可
几个小疑问的解答
问:
为什么要删掉重新创建,而不是在原来的基础上增加内存?
答:
扩容后哈希算法的mask会变,导致计算出来的哈希地址也会变更容易冲突,而且在原来基础上增加内存不方便。问:
既然已经丢弃掉,为什么还要扩容了?
答:
因为这表明他的方法调用数量可以达到扩容后的容量,所以就扩容,免得下回还需要丢弃掉。问:
为什么调用方法了但是buckets中没有拿到
答:
因为不一定存第一个,哈希算法得到的地址是随机的,如果只调了一个方法,则会存在0,1,2三个地方都有可能。
3.6 验证
下面通过调用方法实际验证一下看看是不是真的会存储进去。
通过LLDB查看当调用一个方法后,会在cache中的bucket存储该方法的sel和Imp,以此就可以做到方法的缓存。
方法创建:
方法调用:
分析过程:
- 跟之前一样,就是查找这个对象的内存
- 这里需要偏移16个字节
- 得到cache之后就可以通过上面学到的cache_t的结构来获取到sel和imp了
- 从源码的分析中,我们知道sel-imp是在cache_t的_buckets属性中(目前处于macOS环境),而在cache_t结构体中提供了获取_buckets属性的方法buckets()
- 获取了_buckets属性,就可以获取sel-imp了,这两个的获取在bucket_t结构体中同样提供了相应的获取方法sel() 以及 imp(pClass)
这里可以看到方法调用后在cache->bucket->sel->imp是可以查找到该方法的实现的
3.7 总结
- cache存储了sel和imp,可以进行快速消息发送,sel-imp存储在哈希表中,通过哈希算法来计算下标。
- cache中的maskAndBuckets中包含有buckets,而buckets包含有多个bucket,每个bucket存储了一个sel和imp的键值对。
- maskAndBuckets存储在哈希表中,cache的存储方式是散列表,也可以说是哈希表,通过哈希算法计算下标来写入数据,所以并没有顺序。
- maskAndBuckets的mask存储在前16位,buckets存储在后44位,中间4位一直为0,中间的4位0是为了再快速查找方法时更快速的跳转到最后一个下标,通过掩码进行计算。
- 对于容量
- 首次创建的容量是4
- 当存储的occupied+2大于容量的3/4时就会扩容(这是考虑到多线程)
- 扩容并不是在原来内存的基础上扩展内存,而是删除旧空间,创建新空间,因此每次扩容都需要清除之前的缓存空间,清除缓存的sel和imp。
- 一次扩容是原来容量的2倍,容量最多是2的16次方
- 哈希算法
- sel与上mask就可以得到哈希地址
- 如果冲突了,就需要调用哈希冲突算法
- 哈希冲突算法为i:i-1?mask
- 也就是向前移动一位去存储,如果一直移动到第一位仍然冲突,就从最后一位开始插入
- 如果一直找到begin还在冲突则直接退出
4、 bits
从上文我们得知bits存储了类信息,有成员变量、属性、实例方法、协议。那么具体是怎么存储的呢,需要查看底层结构
4.1 bits的认识
可以看到bits只有一个作用,就是用它来获取class_rw_t格式的数据,因此我们就需要分析class_rw_t。
源码:
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
private:
//获取数据,为class_rw_t
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
4.2 class_rw_t
源码:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
//属性 ro或rwe,这是一个结构体,包含ro和rwe
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
private:
using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>;
const ro_or_rw_ext_t get_ro_or_rwe() const {
return ro_or_rw_ext_t{ro_or_rw_ext};
}
void set_ro_or_rwe(const class_ro_t *ro) {
ro_or_rw_ext_t{ro}.storeAt(ro_or_rw_ext, memory_order_relaxed);
}
void set_ro_or_rwe(class_rw_ext_t *rwe, const class_ro_t *ro) {
// the release barrier is so that the class_rw_ext_t::ro initialization
// is visible to lockless readers
rwe->ro = ro;
ro_or_rw_ext_t{rwe}.storeAt(ro_or_rw_ext, memory_order_release);
}
class_rw_ext_t *extAlloc(const class_ro_t *ro, bool deep = false);
public:
void setFlags(uint32_t set)
{
__c11_atomic_fetch_or((_Atomic(uint32_t) *)&flags, set, __ATOMIC_RELAXED);
}
void clearFlags(uint32_t clear)
{
__c11_atomic_fetch_and((_Atomic(uint32_t) *)&flags, ~clear, __ATOMIC_RELAXED);
}
// set and clear must not overlap
void changeFlags(uint32_t set, uint32_t clear)
{
ASSERT((set & clear) == 0);
uint32_t oldf, newf;
do {
oldf = flags;
newf = (oldf | set) & ~clear;
} while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
}
//得到rwe
class_rw_ext_t *ext() const {
return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>();
}
//开辟rwe的空间
class_rw_ext_t *extAllocIfNeeded() {
auto v = get_ro_or_rwe();
if (fastpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>();
} else {
return extAlloc(v.get<const class_ro_t *>());
}
}
class_rw_ext_t *deepCopy(const class_ro_t *ro) {
return extAlloc(ro, true);
}
//获取ro
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
//如果存在class_rw_ext_t,则从rwe中查找ro
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>()->ro;
}
//否则直接查找
return v.get<const class_ro_t *>();
}
//第一次从内存中加载给rw设置数据的时候,需要设置ro
void set_ro(const class_ro_t *ro) {
auto v = get_ro_or_rwe();
//为什么有可能会有class_rw_ext_t?????
if (v.is<class_rw_ext_t *>()) {
v.get<class_rw_ext_t *>()->ro = ro;
} else {
set_ro_or_rwe(ro);
}
}
//获取所有的方法列表的数组
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->methods;
} else {
return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
}
}
//获取所有的属性列表的数组
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->properties;
} else {
return property_array_t{v.get<const class_ro_t *>()->baseProperties};
}
}
//获取所有的协议列表的数组
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
}
}
};
下面对结构体的成员和函数进行分析:
- ro_or_rw_ext是一个PointerUnion结构体,存储有ro和rwe,这个结构体提供了一些函数可以对存储的ro和rwe进行操作。可以看到这个结构体提供了storeAt函数和get()函数。
using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>;
template <class PT1, class PT2>
class PointerUnion {
uintptr_t _value;
static_assert(alignof(PT1) >= 2, "alignment requirement");
static_assert(alignof(PT2) >= 2, "alignment requirement");
struct IsPT1 {
static const uintptr_t Num = 0;
};
struct IsPT2 {
static const uintptr_t Num = 1;
};
template <typename T> struct UNION_DOESNT_CONTAIN_TYPE {};
uintptr_t getPointer() const {
return _value & ~1;
}
uintptr_t getTag() const {
return _value & 1;
}
public:
explicit PointerUnion(const std::atomic<uintptr_t> &raw)
: _value(raw.load(std::memory_order_relaxed))
{ }
PointerUnion(PT1 t) : _value((uintptr_t)t) { }
PointerUnion(PT2 t) : _value((uintptr_t)t | 1) { }
//存储数据
void storeAt(std::atomic<uintptr_t> &raw, std::memory_order order) const {
raw.store(_value, order);
}
template <typename T>
bool is() const {
using Ty = typename PointerUnionTypeSelector<PT1, T, IsPT1,
PointerUnionTypeSelector<PT2, T, IsPT2,
UNION_DOESNT_CONTAIN_TYPE<T>>>::Return;
return getTag() == Ty::Num;
}
//得到某个数据
template <typename T> T get() const {
ASSERT(is<T>() && "Invalid accessor called");
return reinterpret_cast<T>(getPointer());
}
template <typename T> T dyn_cast() const {
if (is<T>())
return get<T>();
return T();
}
};
- 提供了一些函数可以get和set成员ro_or_rw_ext。下面是get和set的函数
const ro_or_rw_ext_t get_ro_or_rwe() const {
return ro_or_rw_ext_t{ro_or_rw_ext};
}
void set_ro_or_rwe(const class_ro_t *ro) {
ro_or_rw_ext_t{ro}.storeAt(ro_or_rw_ext, memory_order_relaxed);
}
void set_ro_or_rwe(class_rw_ext_t *rwe, const class_ro_t *ro) {
// the release barrier is so that the class_rw_ext_t::ro initialization
// is visible to lockless readers
rwe->ro = ro;
ro_or_rw_ext_t{rwe}.storeAt(ro_or_rw_ext, memory_order_release);
}
- 提供函数ext()获取class_rw_ext_t
调用了私有的函数get_ro_or_rwe()并通过结构体来获取其中的class_rw_ext_t。
//得到rwe
class_rw_ext_t *ext() const {
return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>();
}
- 提供函数ro()获取class_ro_t
会先判断是否有rwe,如果存在,先到rwe中查询ro,如果不存在,就直接获取rw中的ro,这个顺序不要搞混了。(虽然还并不清楚苹果为什么要这样做)
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
//如果存在class_rw_ext_t,则从rwe中查找ro
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>()->ro;
}
//否则直接查找
return v.get<const class_ro_t *>();
}
- 还需要注意一个函数extAllocIfNeeded(),它是用来开辟rwe的空间,当给类附着分类数据时,或运行时创建的方法、属性时会使用它来创建rwe
//开辟rwe的空间
class_rw_ext_t *extAllocIfNeeded() {
auto v = get_ro_or_rwe();
if (fastpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>();
} else {
return extAlloc(v.get<const class_ro_t *>());
}
}
- 获取方法列表的数组、属性列表的数组、协议列表的数组
这里的顺序也要注意,可以看到也是先判断rwe中的methods,如果没有rwe,才获取ro中的methods
//获取所有的方法列表的数组
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->methods;
} else {
return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
}
}
//获取所有的属性列表的数组
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->properties;
} else {
return property_array_t{v.get<const class_ro_t *>()->baseProperties};
}
}
//获取所有的协议列表的数组
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
}
}
注意:
- rw中并没有给提供获取成员变量的函数,只可以获取方法、属性和协议
- 关于类信息的成员只有ro_or_rw_ext,这也说明rw本身也不存储数据,而是存储在ro和rwe中
- rw表示可以读写,可以获取也可以存储数据
- 对于所有的数据都需要先判断是否存在rwe,如果存在就先获取rwe中的数据,如果不存在再获取ro中的数据
4.3 class_ro_t的认识
源码:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
//这个是干什么的
const uint8_t * ivarLayout;
const char * name;
//方法、协议、属性
method_list_t * baseMethodList;//基础方法列表
protocol_list_t * baseProtocols;//基础协议列表
const ivar_list_t * ivars;//成员变量列表,这里变量没有写base,也可以看出这个是不变的
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;//基础属性列表
// This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
_objc_swiftMetadataInitializer swiftMetadataInitializer() const {
if (flags & RO_HAS_SWIFT_INITIALIZER) {
return _swiftMetadataInitializer_NEVER_USE[0];
} else {
return nil;
}
}
method_list_t *baseMethods() const {
return baseMethodList;
}
class_ro_t *duplicate() const {
if (flags & RO_HAS_SWIFT_INITIALIZER) {
size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
class_ro_t *ro = (class_ro_t *)memdup(this, size);
ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
return ro;
} else {
size_t size = sizeof(*this);
class_ro_t *ro = (class_ro_t *)memdup(this, size);
return ro;
}
}
};
说明:
- 它有基础方法列表、基础协议列表、成员变量列表、基础属性列表
- 为什么要提基础二字,这是因为当类加载进内存时的类的数据就存储在这里,而至于分类附着到类上的数据以及运行时动态创建的数据都不存储在这里,而是存储在rwe中。
- 也就是说ro中只存储类本身的数据
- ro中包含有成员变量列表,因为它只存储在ro中,而不存在与rwe中,因此它是只读的,不可写入。
- 我们知道在rw中并没有获取成员变量的入口函数,所以我们只能通过ro来间接获取
4.4 class_rw_ext_t
源码:
/*
这里包含了ro
新增的只有方法、属性、协议,没有变量
这里存放的是所有的,包含目标类的
*/
struct class_rw_ext_t {
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
说明:
- 可以看出来在rwe中也包含有ro,也就是说rwe并不单单只有分类或运行时新增的数据,也包含类的基础数据。
- 同时我们在rw的获取方法列表、属性列表、协议列表的函数中可以看到,其实在rwe的方法列表、属性列表、协议列表中也包含有ro的基础列表。这个其实在后面分析类的加载过程的时候更能清晰的看出来会将ro中的数据直接copy到rwe中
- 也就是说ro的数据其实在三处地方可以获取到,1)直接通过rw->ro->methods;2)通过rw->rwe->ro->methods;3)rw->rwe->methods
4.5 列表数据
后面会专门分析协议,此处不分析协议了。
4.5.1 列表数组
列表数组格式一样,只以方法列表数组为例。方法列表为method_list_t,方法为method_t。
源码:
class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
public:
method_array_t() : Super() { }
method_array_t(method_list_t *l) : Super(l) { }
method_list_t * const *beginCategoryMethodLists() const {
return beginLists();
}
method_list_t * const *endCategoryMethodLists(Class cls) const;
method_array_t duplicate() {
return Super::duplicate<method_array_t>();
}
};
4.5.2 方法列表、成员变量列表、属性列表
都是以entsize_list_tt结构体存储的,这个结构体的样式后面再分析
// Two bits of entsize are used for fixup markers.
//entsize的两位用作固定标记
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
bool isUniqued() const;
bool isFixedUp() const;
void setFixedUp();
//返回某个方法的下标
uint32_t indexOfMethod(const method_t *meth) const {
uint32_t i =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
ASSERT(i < count);
return i;
}
};
struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
//增加了一个功能,就是判断是否存在这个成员变量
bool containsIvar(Ivar ivar) const {
return (ivar >= (Ivar)&*begin() && ivar < (Ivar)&*end());
}
};
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
4.5.3 方法、属性、成员变量结构体
4.5.3.1 method_t
源码:
/*
1、方法选择器
2、方法类型
3、函数指针
*/
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
//这种写法方式没看懂
//但是比较明显的是左<右,就返回YES,这是基本的一个排序操作
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
说明:
- 一个方法中包含有方法选择器sel、方法类型、函数指针imp
- 还提供了一个函数用以排序。这个将在类的加载时会用到,判断两个方法的顺序,这里先记一下有这么个玩意
4.5.3.2 ivar_t
源码:
struct ivar_t {
#if __x86_64__
// *offset was originally 64-bit on some x86_64 platforms.
// We read and write only 32 bits of it.
// Some metadata provides all 64 bits. This is harmless for unsigned
// little-endian values.
// Some code uses all 64 bits. class_addIvar() over-allocates the
// offset for their benefit.
#endif
int32_t *offset;//偏移量,这个是干什么的
const char *name;//名称
const char *type;//类型
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;//多少字节对齐
uint32_t size;//大小
//获取当前的对齐字节数
uint32_t alignment() const {
//8字节对齐
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
//否则按照设置的字节进行对齐
return 1 << alignment_raw;
}
};
说明:
- 成员变量只有变量名称和类型,没什么额外的数据
4.5.3.3 property_t*
源码:
//名称和attributes
//以后要区分property和attributes,属性是有自己的一些属性的比如copy/strong
struct property_t {
const char *name;
const char *attributes;
};
说明:
- 属性除了属性名称,就是attributes,它存储的就是比如copy、strong等等。
4.6 验证
通过LLDB查看bits中的所有类信息与我们所定义的类的信息是否是一致的。
通过指针偏移得到bits数据,再分别得到rw中的rwe和ro,通过这种方式就可以获取到所有的方法列表、协议列表、属性列表、成员变量列表。
4.6.1 获取bits
isa属性: 继承自objc_object的isa,占8个字节
Class superclass: 是Class类型,是一个指针,占8个字节
cache
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
#if __LP64__
uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
这里只看真机64位:
_maskAndBuckets 是uintptr_t类型,它是一个指针,占8字节
_mask_unused 是mask_t 类型,而 mask_t 占4字节
_flags是uint16_t类型,uint16_t是 unsigned short 的别名,两个字节
occupied的大小也是uint16_t类型,uint16_t是 unsigned short 的别名,两个字节
计算:
- 所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16字节
结论: 从首地址需要平移32个字节大小得到bits
4.6.2 开始获取
【第一步】通过指针偏移得到bits
说明:
- 因为需要偏移32个字节,而这里是16进制的数值,所以需要从0x100002250变为0x100002270。
【第二步】得到rw
说明:
- p *3->这种来获取,指针函数就用->)
- p *$4就是这个数据的所有对象信息
- 所有的信息都存储在ro_or_rw里
【第三步】获取方法列表
- p $4.methods()得到方法信息
- p $5.list得到方法列表
- p *$6这里获取到的是方法列表的第一个数据。所以是第一个方法
- 也可以通过p $7.get(0)来获取第一个方法
【第四步】获取属性列表
过程与方法列表一样,而且在上文已经详细查看了rw的底层实现,所以直接看过程,不再赘言了。
【第五步】获取成员变量列表
可以看到需要先获取ro,之后通过ro来获取成员变量列表,因为rw中并没有提供获取成员变量的入口函数
4.7 总结
- OC中类的底层结构是objc_class,继承自objc_object结构体
- objc_class中的bits存储了属性、方法、协议、成员变量,其中成员变量并不直接存储在class_rw_t中,而是存储在class_ro_t中,是干净内存
- class_rw_t可以得到类的所有信息,包括ro和rwe
- rw中获取数据,先判断是否存在rwe,若存在rwe则通过rwe获取,否则通过ro获取
- ro是干净内存,类的加载时类本身的数据存放在ro中
- rwe是脏内存,运行时动态创建成员或分类中的成员都在rwe,包括属性、方法、协议。
- rwe中的属性、方法、协议列表也包括ro中的数据
- rwe结构体中包含有ro
5、简单总结
- OC中类的底层结构是objc_class,继承自objc_object结构体
- objc_class结构体中中包括四个属性,isa、superClass、cache、bits
- isa继承自objc_object,包含有元类信息
- superClass也是类结构,表示父类
- cache是缓存的方法列表,通过哈希表存储sel和imp,在进行快速消息发送时在cache中通过sel查找imp
- cache的存储和获取都需要通过哈希算法来计算
- bits存储有类信息,包含属性、方法、协议、成员变量,bits的数据是rw,但是分成两种类型存储,一种是干净内存ro只存储类本身的数据,一种是脏内存rwe存储分类数据和运行时动态创建的数据
- 元类是由编译期定义和创建的,用来管理类,是类对象的类。类方法在元类中以对象方法的姿态存在。
- 元类本身我们无法看到,无法直接使用,可以通过类的isa查看元类信息
- 继承关系是类和元类的关系,不是对象的关系,根元类继承自NSObject类,NSObject类的父类是nil,也就是没有父类,NSObject是万物起源。
- 对象的isa指向类,类的ISA指向元类,元类的isa指向根元类,根元类的isa指向自己
- NSObject类的isa也指向根元类
通过LLDB验证过程中使用到了指针偏移,有疑惑的地方可以看指针偏移原理分析链接