不管你学习的有多慢,只要不原地踏步那也是一种进步!
前言
上一章节中探索了对象的本质和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
里面存储了属性,对象方法,协议等数据。