不管你学习的有多慢,只要不原地踏步那也是一种进步!
前言
上一章节中探索了对象的本质和isa关联类的过程,了解到对象的本质是一个结构体,而且是通过其内部成员变量isa关联到类的,那类又是个什么东西呢?那就进入本章节的探讨内容。
类(Class)
在OC中万物皆对象,而对象中都会存在一个isa指向类,那么类的isa又是指向哪呢,有什么作用呢,下面通过代码和lldb调试来看一看:
isa指向
-
对象
isa指向
创建一个LhkhPerson类,实例化一个对象p
这边验证了一下对象的isa指向类 对象isa ---> 类,接着得到类的指针地址0x0000000100794520,得到类的地址,那么是不是也可以看一下类的结构呢?接着lldb调试:
-
类
isa指向
通过lldb继续调试上面获取到的类的地址,得到类的isa,然后也通过与上掩码0x00007ffffffffff8,得到一个新的地址**0x00000001007944f8**,输出得到也是LhkhPerson,所以得出类的isa--->与类同名的一个类。
-
类VS同名类
这就让我们百思不得其解了,都是LhkhPerson,为什么指针地址不一样呢?到底哪个才是LhkhPerson真正的指针地址呢?接下来通过代码验证一下:
通过上面这段代码打印发现都是0x0000000100794520,那可以确定LhkhPerson的指针地址就是这个,那0x00000001007944f8这个地址到底是什么呢?
-
元类诞生
这个时候需要通过MachOView工具来分析一下machO可执行文件
然后右击
show in Finder ,再右击显示包内容,将可执行文件拖入到MachOView工具上,
通过MachOView分析可以看到在项目编译过后,通过查看符号表,发现系统给LhkhPerson生成了一个元类,而上面遗留的那个问题对应的类正是这个元类。
-
元类总结
- 通过上面分析,
元类是由系统编译过后产生的,类的名称和其关联的元类的名称是一样的; - 对象的
isa指向类,类的isa指向元类。
- 通过上面分析,
-
元类
isa指向
那元类有没有类结构呢,或者其isa又是指向谁呢?接着这个问题我们往下走:
继续通过
lldb发现元类的isa指向根元类,而根元类的isa指向根元类自己。
-
isa指向总结
通过上面代码调试和MachOView工具的分析最终得到了isa的指向:
对象---isa--->类---isa--->元类---isa--->根元类---isa--->根元类
根据这个可以得到一个isa指向图
isa的指向图探索出来了,我们一般在项目中都会有类继承,那么class是怎么继承1的呢?
class类的继承链
- 类继承链
LhkhTeacher是继承自LhkhPerson,LhkhPerson是继承自NSObject,通过打印LhkhTeather和LhkhPerson还有NSObject的父类,得出它们之间的继承链为LhkhTeather---继承--->LhkhPerson---继承--->NSObject
既然类都有一条继承链,那么元类呢?
- 元类继承链
LhkhPerson继承自NSObject,打印可以得到其元类,根元类,根根元类地址都是0x7fff80030638,而LhkhPerson元类的父类打印地址结果也为0x7fff80030638,并且得到这个地址指向NSObject,即根元类;那么是不是可以得到任何一个对象的元类的父类就是根元类呢?
LhkhTeacher是继承自LhkhPerson,LhkhPerson继承自NSObject,那如果根据上面的结论就是LhkhTeacher的元类的父类也是根元类,但是打印结果告诉我们,并非如此,子类的元类的父类是就是子类的父类的元类,为什么会这样呢?因为元类也是有一条继承链,LhkhTeacher元类---继承--->LhkhPerson元类---继承--->根元类NSObject。
到这相信大家就会出现一个疑问,类继承链会到达根类NSObject,元类的继承会到达根元类NSObject,那么根类NSObject和根元类NSObject继承自什么呢?
- 类和元类继承特殊情况
根据打印结果可以得到,根类的父类为null,即根类没有父类,而根元类的父类为根类。
- 类继承链总结
子类---继承--->父类---继承--->根类--->nil子类元类---继承--->父类元类---继承--->根元类---继承--->根类--->nil根据这个可以得出类的继承图
- Apple官方 isa和类的走向图
类的底层结构
探究类的底层结构前我们需要先了解一下内存偏移
内存偏移补充
探讨这个我们通过普通指针,对象指针,数组指针三个例子来看:
1. 普通指针
定义了两个局部变量
a和b都等于10,通过打印可以看到它们的值都是10,而它们的地址是不同的,a的地址为0x7ffee9501c24,b的地址为0x7ffee9501c20
则得出
- 根据
a和b为局部变量,和其地址0x7开头我们可以知道其存储在栈区域; - 栈区域地址是由高指向低地址,而
a和b相差4字节,因为a为int型,需要占用4字节,所以a到b的偏移量为4字节。
2.对象指针
定义了两个对象obj1和obj2,通过打印的地址我们分析一下:
- 可以看出
obj1的地址为0x600002440100是通过alloc开辟出来的,而obj2的地址为0x600002440110也是通过alloc开辟出来的,通过0x6开头和他们是手动开辟的我们知道他们是存在堆区的; - 再看两者的取地址,也就是指向
obj1和obj2堆地址的地址,分别是0x7ffeec5bcc08和0x7ffeec5bcc00,可以看出是在栈区,且由于obj1位对象占用8字节,所以obj1到obj2的偏移量为8字节。
3.数组指针
定义一个数组指针
c,和一个指针d,通过打印可以得到数组指针c的地址为0x7ffeebfa1c20,而c的第一个元素指针地址也为0x7ffeebfa1c20,后面的元素地址也是每隔4个字节(因为数组指针里面存的是int型数据,int型占4个字节);而指针d初始赋值是c,所以打印d地址就是0x7ffeebfa1c20,而d偏移一个步长(这边的步长也是由c里面的元素类型决定,我们这边存的是int型,所以偏移4字节);通过上面你的分析我们可以得到:
- 数组的首地址就是其第一个元素的地址,也就是内存地址的
首地址就是第一个元素的地址; - 数组里面的下一元素的地址可以通过首地址加上上一个元素偏移量(取决于数组里面存的元素类型)得到,也就是内存偏移可以根据
首地址+偏移量方法获取相对应变量的地址。
类结构
在探索对象的本质的时候就已经发现了class的本质其实是一个 objc_Class *结构体指针,在objc源码中直接搜索objc_Class *可以得到;
那么
objc_Class又是个什么呢?再次搜索一下objc_Class 挥发想能找到两个定义
这一个注意在objc2已经不使用了,所以直接看下面这个
可以发现
objc_Class是继承自objc_object,
通过分析源码,可以发现除了一些方法,其内部有4个成员变量,
ISA,superclass,cache,bits:
ISA:这个是一个隐藏属性,是继承于objc_object里面的成员变量isa,是一个结构体指针,占用8个字节,主要是用于关联类的;superclass:也是一个Class类型,那么本质也就是结构体指针,也就是当前类的父类,也是占用8个字节;cache:是一个cache_t类型,本质是一个结构体,用于优化方法调用,这个后面章节会讲解;bits:是一个class_data_bits_t类型,本质也是一个结构体,主要用来存储类的属性,方法等数据的,这也是我们需要探讨的重点。
那我们想要探讨bits里面的内容,必须要获取到其内存地址,根据上面的内存偏移,想要得到bits的地址,还差一个cache的内存大小,由于cache_t是结构体所以其内存大小取决于内部变量,所以我们暂时不知道大小那我们先来看看cache_t其内部结构:
cache内存大小
通过源码中
command左击cache_t进入到cache_t这个结构体中,有很多的方法和static修饰的变量,由于方法和全局变量都不是在计算结构体内存之中,无关重要,所以这边我们只需要看下面这两个变量:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask:是一个泛型的结构体,真正的大小由uintptr_t(无符号长整型,占用8字节)决定,所以可以得到其占用8字节;- 联合体
union:里面只有一个变量一个结构体,- 这个结构体中有3个变量:可以得出结构体大小为
8字节_maybeMask:一个mask_t(uint32_t)型的结构体 占用4个字节_flags:uint16_t类型 占用2字节_occupied:uint16_t类型 占用2字节
_originalPreoptCache:是一个结构体指针类型,占用8字节 而联合体是里面是互斥的,那么可以得到联合体大小为8字节。
- 这个结构体中有3个变量:可以得出结构体大小为
union {
struct {
explicit_atomic<mask_t> _maybeMask; //4
#if __LP64__
uint16_t _flags; //2
#endif
uint16_t _occupied;//2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
}
所以通过上面分析可以得到cache_t占用8+8,也就是16个字节,那么我们就可以计算得到bits的地址了,也就是首地址+32个字节的偏移量(8+8+16)。
bits探究
通过查找发现
class_rw_t *data() const {
return bits.data();
}
这个方法里面返回bits.data()也就是我们bits里面的数据,那我们需要查看bits里面的内容,就先来看看class_rw_t。
- class_rw_t探究
通过源码,我们进入到class_rw_t里面,里面有很多的方法,我们发现几个我们熟悉的:
这里面存储这我们的方法,属性,协议等数据,那我们通过
lldb来探索一下:
- 我们通过
x/4gx p.class获取到类的首地址为0x100008b20; - 然后通过上面探究出来的要想获取到
bits的地址需要偏移32个字节,也就是0x20,并强转成class_data_bits_t*,得到地址为$2 = 0x0000000100008b40; - 通过
return bits.data(),我们也通过p $2->data()得到(class_rw_t *) $3 = 0x000000010126f810,到此我们已经得到了class_rw_t的指针地址; - 通过指针地址取值
*$3最终得到class_rw_t里面的数据。
那既然得到了class_rw_t数据结构了,我们又知道他内部存储了方法,属性,协议等,我们就来看看:
这个是LhkhPerson里面的一些属性和方法
- 属性
- 通过上面得到的
class_rw_t的地址我们取出值,然后获取其属性列表p $4.properties(); - 然后就一层一层往里面取
p $5.list,p $6.ptr,然后再通过地址取值p *$7这时候就真正得到了我们属性的列表; - 通过
p $8.get(0)获取第一个属性,得到我们的name,和我们定义的是一样的。
- 成员变量补充
我们上面探究属性的时候没有添加成员变量,那么会不会成员变量也会存在属性表中呢?
我们这边发现只能打印出我们的
name属性,再取的时候就会越界错误,所以成员变量nickName没有存在属性表中,那么存在哪呢?
我们通过查找class_rw_t的方法实现时发现里面还存在一个方法class_ro_t,我们顺着进去查看实现,
通过查看class_ro_t方法实现我们发现其存在一个ivar_list_t *型变量ivars,这个词我们应该很熟悉啊,这不就是实例变量表吗,那我们大概知道了实例变量应该就存在这个表里面了,我们来验证一下看看
- 我们通过之前的步骤获取到
class_rw_t的地址并取值,然后通过p $4.ro()获取到class_ro_t地址; - 我们通过
class_ro_t地址取到值,然后通过p $6.ivars获取到实例变量列表的地址,并通过p *$7取值; - 通过
p $8.get(0)取到里面的实例变量,成功打印了我们的nickName成员变量; - 我们继续取值发现里面还存了
_name,而我们知道这个name是我们定义的属性,所以我们还可以得到,系统给属性生成的_属性名的成员变量并存在了class_ro_t的ivar_t中。
总结
在class_rw_t方法中会调用class_ro_t这个方法,并通过里面的ivars(ivars是ivar_list_t这个类型,这是一个泛型,实质是ivar_t)实例变量表ivar_t存储成员变量和系统生成的属性的_属性名的变量。
- 方法 我们使用同样方式探索一下方法列表:
我们这边获取到方法名的时候有个注意点,由于
method_t系统做了处理,是一个结构体,而他把方法名等数据放到了他里面的一个结构体变量big下,所以这边我们取到方法名时后面需要带上一个.big().
- 类方法补充
我们继续获取方法,发现属性的getter和setter方法过后,在获取就报错越界了,但是并没有获取到我们声明的+(void)sayHello类方法,那我们声明的那个类方法呢?
其实通过上面的对象方法探究我们知道对象方法存在类当中,那么我们知道OC中万物皆对象,那我们猜想类方法会不会就存在元类当中呢?
我们通过p/x object_getClass(LhkhPerson.class)获取到LhkhPerson的元类,然后按照上面探讨对象的方法一样会中获取到类方法+(void)sayHello,所以我们可以得到:
对象方法存在类结构的bits里面,而类方法存在元类结构的bits里面。
总结
通过上面的探究我们可以知道类的底层结构主要是四个变量ISA,superClass,cache,bits,而我们的bits里面存储了属性,对象方法,协议等数据。