底层研究 - 类的底层探索(上)
前言
在底层研究 - 对象的底层探索(下)中我们已经知道对象的isa指向了类对象,那接下来我们探索下类对象的底层。
1、类对象、元类、根元类
在objc中,万物皆对象,因此我们也不难得出一个结论:类也有一个isa指针,通过源码验证下
通过源码可以发现类的定义是一个继承自objc_object的结构体objc_class,这也证明了我们上述的推测。objc_class主要由isa、superclass、cache、bits四个属性,我们接下来分别探索下这四个属性。
1.1 isa指针
获取类对象的方式有三种,分别是:
- 直接调用类的class方法
- 调用实例化对象的class方法
- 使用objc提供的object_getClass方法
通过打印的结果,可以发现上述的三种方式都是指向同一个的地址,也就是三者是等价的。那么我们可以得出一个结论
类的信息在内存中永远只存在一份,所以类对象只有一份。
那么这个isa指针会指向什么呢,我们创建个对象跟踪下:
根据探索的结果,我们可以发现类对象的isa指向了一个Person类,这个Person类地址跟类对象不一致,也就是说这是个新地址,其实这就是元类,元类的定义和创建都是编译器自动完成的。元类的isa又指向了NSObject类,而这个NSObject又指向了它自身,也就是说NSObject就是根元类
isa指针路径
那么存在继承关系的子类,它的isa又是怎样的呢,跟父类会存在“羁绊”么,新建一个People类继承于Person类探索下:
people isa指针
事实证明,即使存在继承关系,子类和父类的isa是相互独立的,唯一相同的是都指向了根元类NSObject。
1.2 superclass
既然isa并没有体现继承关系,那么是用什么来保存继承关系呢,从objc_class源码中发现了isa指针后的superclass,那接下来我们探索下superclass。
我们从探索的结果可以发现类的superclass走向,那元类的superclass呢,我们还是用People的实例对象探索下:
元类的superclass
通过上面两个截图,我们可以发现:
- 类的superclass和元类的superclass 最后都指向了相同的NSObject。
- 通过地址的对比,可以发现元类的父类就是父类的元类。
经过上面探索,我可以知道了类对象本质为objc_class结构体,同时分别对isa和superclass的探索,其实我们可以得到一张很经典的图:
1.3 bits
接下来我们探索下class_data_bits_t bits,但在探索前,我们先来了解下内存平移。
通过打印结果,我们可以发现d是个指针,它指向了数组c的首地址,d+1则表示了指针平移数组c所定义的int类型大小的字节,也就是4个字节。因此通过循环可以正确获取c数组的地址和内容,这也就是内存平移。
了解完内存平移之后,我们开始探索下bits究竟有何作用。根据objc_class的成员变量,isa占8字节,superclass占8字节,cache占16字节,也就是说从类对象的首地址平移32个字节就可以获得bits的首地址。我们还是先创建一个Person类,然后实例化对象
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject {
NSString *_hobby;
}
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,assign) int age;
- (void)instanceMethod;
+ (void)classMethod;
@end
NS_ASSUME_NONNULL_END
bits内存平移图
通过探索我们发现,理想很丰满,现实很骨感,通过 class_data_bits_t 转义取出来的值压根就没法看懂!!!(╯‵□′)╯︵┻━┻,只能从源码入手了~~
看下源码中class_data_bits_t的定义
首先发现有一个friend修饰的objc_class,这是一个友元类声明,也就是说objc_class能直接访问bits的私有成员。其次也就一个bits成员,并没有发现什么实际性的东西,那么我们从提供的方法中挖掘下
class_data_bits_t public
可以发现class_data_bits_t 公开的方法中有返回了 class_rw_t类型 的data方法,那么我们尝试下获取data
class_data_bits_t data
可以发现是能正常获取data,但data内存的东西通过打印同样没法直接看懂,只能翻下源码中class_rw_t的定义
class_rw_t public
在class_rw_t所定义的public方法中,发现了三个函数,从方法名可以推测是返回的方法,属性,协议。我们接下来尝试在data中获取方法
1.3.1 获取对象方法
获取方法经过层层下探,最终得到了method_list_t类型的数据,通过查询源码,可以发现它是一个entsize_list_tt模板类,而类中存在get方法
method_list_t
entsize_list_tt
直接调用下get看能返回什么结果
entsize_list_tt get
结果发现是一个method_t的类型,我们通过源码看看method_t是啥
method_t
method_t里面还有两个结构体big和small,根据源码可以猜测是根据大小端模式存储不同的数据类型,因为本次程序探索的终端是intel芯片电脑,属于大段模式,所以可以通过提供的big方法来获取方法信息
method_t big
调用big
通过打印,可以发现一开始我们打印出来的$8里面的count = 6实际上就是这个Person对象存在多少个方法。
但是,我们发现并没有打印出类方法classMethod,也就是说类方法并不是存在于类对象之中,同时还发现多了一个.cxx_destruct方法,这是当类中有成员变量时就会自动生成该方法(包括属性自动生成的成员变量),主要是用于在ARC模式下,释放成员变量。
1.3.2 获取对象类方法
那么消失的类方法classMethod究竟存在哪呢,结合上文的探索,我们发现类对象和元类对象都是Person,那么可以推测可能是存在于它的元类对象中,按照实例方法的探索过程再探索一次元类对象,打印的结果也证实了这一点。
那么这里我们就有一个疑问,为什么类方法和实例方法要分别存储在类和元类呢?或者说这么做有什么好处?
- 其实元类存在的目的,就是为了复用消息机制。
在OC中调⽤⽅法,其实是在给某个对象发送某条消息。消息的发送在编译的时候编译器就会把⽅法转换为objc_msgSend这个函数。id objc_msgSend(id self, SEL op, ...) 这个函数有俩个隐式的参数:消息的接收者,消息的⽅法名。通过这俩个参数就能去找到对应⽅法的实现。
objc_msgSend函数就会通过第⼀个参数消息的接收者的isa指针,找到对应的类,如果我们是通过实例对象调⽤⽅法,那么这个isa指针就会找到实例对象的类对象,如果是类对象,就会找到类对象的元类对象,然后再通过SEL⽅法名找到对应的imp,然后就能找到⽅法对应的实现。
那如果没有元类的话,那这个objc_msgSend⽅法还得多加俩个参数,⼀个参数⽤来判断这个⽅法到底是类⽅法还是实例⽅法。⼀个参数⽤来判断消息的接受者到底是类对象还是实例对象。
消息的发送,越快越好。那如果没有元类,在objc_msgSend内部就会有有很多的判断,就会影响消息的发送效率。
所以元类的出现就解决了这个问题,让各类各司其职,实例对象就⼲存储属性值的事,类对象存储实例⽅法列表,元类对象存储类⽅法列表,符合设计原则中的单⼀职责,⽽且忽略了对对象类型的判断和⽅法类型的判断可以⼤⼤的提升消息发送的效率,并且在不同种类的⽅法⾛的都是同⼀套流程,在之后的维护上也⼤⼤节约了成本。
所以这个元类的出现,最⼤的好处就是能够复⽤消息传递这套机制。不管你是什么类型的⽅法,都是同⼀套流程,也就是统一方法调用的接口,保证单一职责原则。
那么类方法和实例方法在底层有什么区别么?
class_getClassMethod
我们通过源码可以发现获取类方法的本质是调用了返回实例方法的函数,也就是说:
- 在objc底层没有类⽅法和实例⽅法的区别,都是函数。
1.3.3 获取对象属性
接下来我们再获取下对象的属性,方式其实跟探索方法也是一样的,因为用的都是一个模板类,我们直接看下结果
对象属性探索
1.3.4 获取对象成员变量
我们在class_rw_t发现并没有能直接获取成员变量的方法,那么很可能就是封装在其它结构体内,通过方法名,我们可以定位到一个ro方法,它返回了一个class_ro_t结构体
那接下来直接打印确认下
成员变量探索
通过打印结果可以发现class_ro_t中存在这ivars,这个在objc中就是用来代表成员变量,直接获取其内容可以发现又是方法用的那个模板类,直接用get方法获取内容
成员变量获取
通过打印,我们也基本确定了之前的推测,成员变量是存储在class_ro_t中,而且还包含了属性自动生成的成员变量。
2 总结
-
类对象
类也是对象,类对象有且只有⼀个。类对象本质为objc_class结构体。类对象⾥⾯存储了类的⽗类、属性、实例⽅法、协议、成员变量、⽅法缓存等等。 -
isa指向关系
实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类⾃⼰。 -
元类的继承关系
⽗类的元类就是元类的⽗类。根元类的⽗类就是NSObject。NSObject是万类之祖。 -
类的信息存储
类中的信息都存储在 class_rw_t 和 class_ro_t 中,通过 entsize_list_tt 模板,可以实例化出method_list_t、ivar_list_t、property_list_t 三种类型。 -
元类的作用
类方法存在元类对象中,实例方法在类对象中,目的是为了复用消息机制,统一方法调用的接口,保证单一职责原则。 -
类方法和实例方法的本质
在objc底层没有类⽅法和实例⽅法的区别,都是函数。 -
.cxx_destruct
.cxx_destruct⽅法是在ARC模式下⽤于释放成员变量的。只有当前类拥有实例变量时这个⽅法才会
出现,property⽣成的实例变量也算,且⽗类的实例变量不会导致⼦类拥有这个⽅法。