前面我们探索分析了对象的底层原理。我们知道在实例对象内都有一个isa
指针,实例对象的isa指针指向的是其所属的类对象
。那么接下来我们就探索一下类的底层原理。
一、类的本质
首先我们看一下类的几种获取方式
Class class1 = [MyClass class];
Class class2 = [MyClass alloc].class;
Class class3 = object_getClass([MyClass alloc]);
NSLog(@"\n%p-\n%p-\n%p",class1,class2,class3);
通过打印结果看三者的内存地址相同,证明我们的类对象有且只有一个
。既然我们知道类对象是Class类型,我们就先去objc源码里查找一下Class类型的声明
从源码里我们看到Calss的声明,Class类型是一个objc_class的结构体指针
,也就说明了类的本质是一个objc_class的结构体
。
二、isa分析
再去objc源码里找一下objc_class
这个结构体
在源码里我们看到objc_class继承自objc_object
,之前我们探索对象的底层原理时得知对象的本质是一个objc_object的结构体,也就证明类也是一个对象,类对象的isa指针继承自objc_object这个结构体。我们知道实例对象isa指针指向它的类对象
,那类对象的isa指针指向哪里呢?我们去查找一下
打印一下p对象的内存地址
通过对p对象的isa指针进行位运算 或者 & ISA_MASK掩码 得到MyClass类对象
我们在打印一下MyClass类对象的内存地址,获取类对象的isa指针
通过对 类对象的isa指针 & ISA_MASK掩码,看一下类对象的isa指针指向的内存地址
从打印结果我们发现也是显示一个MyClass,刚才我们证明了类对象有且只有一个,那么我们看一下这个MyClass是不是MyClass类对象
从打印结果来看,此时打印显示的MyClass,并不是MyClass类对象。这就是我们所说的元类。 关于元类:
1.元类对象和类对象的名字是一样的。
2.元类和类是一样的,也是一个objc_class的结构体。
证明了类对象的isa指针指向元类对象
,我们继续查看元类的isa指针
同理通过元类的isa指针 & ISA_MASK掩码,发现元类的isa指针指向一个叫NSObject的东西,我们打印了NSObject类对象地址对比发现其并不是NSObject类对象(根类),此时我们猜想这是不是NSObject元类(根元类)呢,我们再打印一下NSObject的内存地址
从打印结果我们看出,元类的isa指针指向的内存地址和根元类的内存地址相同,也就是说元类的isa指针指向根元类
,同时也证明根类的isa指针指向根元类
,我们再继续看一下根元类的isa指针指向哪里
此时我们发现根元类的isa指针指向的内存地址和根元类的内存地址相同,也就是说根元类的isa指针指向根元类自己
。此时我们可以得出isa的指向流程
三、类与元类的继承关系
知道了isa指向流程,了解了元类与根元类概念。那我们知道类有继承链关系,那么元类有继承链吗?如果有是什么样的关系?我们可以通过以下代码去了解代码中类的继承关系是MySubclass继承自MyClass,MyClass继承自NSObject
NSObject * obj1 = [NSObject alloc]; // NSObject实例对象
Class cls = object_getClass(obj1); // NSObject类对象
Class metaclass = object_getClass(cls); // NSObject元类(根元类)
NSLog(@"NSObject实例对象: %p", obj1);
NSLog(@"NSObject类对象: %p", cls);
NSLog(@"NSObject元类(根元类): %p", metaclass);
Class nSuperCls = class_getSuperclass(cls); //根类NSObject的父类
Class snSuperCls = class_getSuperclass(metaclass); //根元类的父类
NSLog(@"根类NSObject的父类: %@ - %p", nSuperCls, nSuperCls);
NSLog(@"根元类的父类: %@ - %p", snSuperCls, snSuperCls);
Class subMetaCls = objc_getMetaClass("MySubclass"); //MySubclass的元类
Class subMetaSuperCls = class_getSuperclass(subMetaCls); //MySubclass元类的父类
NSLog(@"MySubclass的元类: %@ - %p", subMetaCls, subMetaCls);
NSLog(@"MySubclass元类的父类: %@ - %p", subMetaSuperCls, subMetaSuperCls);
//MySubclass 继承自 MyClass
Class mMetaCls = objc_getMetaClass("MyClass"); //MySubclass的父类(MyClass)的元类
Class mSuperCls = class_getSuperclass(mMetaCls); //MyClass元类的父类
NSLog(@"MySubclass父类(MyClass)的元类: %@ - %p", mMetaCls, mMetaCls);
NSLog(@"MyClass元类的父类: %@ - %p", mSuperCls, mSuperCls);
打印结果如下
观察我们打印的地址信息,可以看到MyClass元类的父类的内存地址和NSObject元类(根元类)的内存地址相同,MySubclass元类的父类和MySubclass父类的元类的内存地址相同。也就是说元类的父类就是父类的元类
。针对NSObject根类来说,根类的元类的父类就是根类自己
。MySubclass的元类继承自MyClass的元类,MyClass的元类继承自NSObject元类(根元类),根元类继承自NSObject类对象,NSObject的父类为null。整理如下图所示
四、类对象的数据结构
1、内存偏移
前面分析证明了类的本质是一个objc_class的结构体,那么我们就看一下objc_class的结构体里面有什么。我们要分析类对象的数据结构,首先我们要先了解一下内存平移的概念。
// 数组指针
int c[4] = {5,6,7,9};
int *d= c;
NSLog(@"%p - %p - %p - %p - %p",&c,&c[0],&c[1],&c[2],&c[3]);
NSLog(@"%p - %p - %p - %p",d,d+1,d+2,d+3);
for (int i = 0; i<4; i++) {
int value = *(d+i);
NSLog(@"%d",value);
}
我们把c这个数组指针赋值给d,同时打印一下c和d,我们输出一下结果
从输出的结果我们可以看出d+1,d+2,d+3
与数组里对应元素的内存地址相同,也就是说我们可以通过这种方式也能获取到数组元素的内存地址。d+1,d+2,d+3这种就叫做内存平移
,每次平移的大小是4个字节。这是因为我们创建的数组是int类型,int数据在内存里是占4个字节,因此我们内存平移的大小是4个字节。因此我们也是可以通过内存平移的方式取出数组里面的值
2、数据结构分析
了解了内存平移的概念之后,我们看一下objc_class
这个结构体
从源码里能看出objc_class
中包含类的四个重要的数据,Class ISA
、Class superclass
、cache_t cache
、class_data_bits_t bits
isa
指针前面已经分析了,结构体内的superclass
里面存储了类对象的父类。cache
中存储了运行中的一些缓存信息,比如消息缓存。从objc_class
提供的方法可以看出,在获取一些数据时,优先从缓存中获取,如果没有缓存,则从bits
中获取。这样设计的目的是保证响应速度
。详细分析可查看类的底层: cache_t详解bits
类的属性、实例⽅法、协议等相关的数据信息都存储在了bits
中。
3、bits解析
引入一个案例,创建一个MyClass类
@interface MyClass : NSObject {
NSString * _hobby;
}
@property (nonatomic, copy) NSString * name;
@property (nonatomic, assign) int age;
- (void)instanceMethod;
+ (void)classMethod;
@end
@implementation MyClass
- (instancetype)init {
self = [super init];
if (self) {
}
return self;
}
- (void)instanceMethod {
NSLog(@"%s", __func__);
}
+ (void)classMethod {
NSLog(@"%s", __func__);
}
@end
设置断点,运行程序,获取MyClass
类对象的内存地址。
我们可以通过对MyClass类对象内存的首地址进行内存平移32个字节的方式获取到bits的内存地址,平移32个字节是因为bits前面的isa
指针和superclass
指针各占8个字节,cache
占16个字节,总共是32个字节。打印出来的类对象的首地址为0x100008258
,平移32个字节就是bits的内存首地址 0x100008278 = 0x100008258 + 0x20
。从源码我们已经知道bits
是class_data_bits_t
类型的结构体,那么我们对此时的地址做一个强转:
通过对强转得到的地址取值发现并不能直接获取到,那么接下来我们再去源码里面看class_data_bits_t
这个类型的结构体里面是什么样子
从源码里我们找到了class_data_bits_t
提供的一个data()
方法,其返回值是一个class_rw_t
类型的结构体,接下来我们对刚才打印得到的指针$2调用一下data()
方法,打印如下
由上图可以看出我们对通过调用data()方法得到的$3取值得到了一些信息,从打印的数据里面我们并没有发现我们想要的信息。再去源码里面看一下class_rw_t
这个结构体
我们从class_rw_t这个结构提源码里面发现了以上的三个方法,从方法名我们也可以看出通过方法methods()、properties()、protocols()
可以获取实例对象的方法列表、属性列表和协议列表。
1. 方法列表(methods)获取
我们先调用一下methods()方法
通过以上四步对查找我们得到method_list_t
的结构体类型数据,我们可以看到里面有个count = 7的信息,这个count就是方法的个数。而MyClass的实例对象的方法我们能看到的有name、age各2个方法(get、set)加上init和instanceMethod总共6个方法,那为什么打印出来的count是7呢?我们再去源码里查找method_list_t
这个结构体,看一下结构体内有没有方法可以让我们获取结构体中包含的每个方法的信息
从源码里可以发现method_list_t继承自entsize_list_tt的结构体
我们在entsize_list_tt
结构体中发现了get(uint32_t i)
方法,通过get(uint32_t i)
方法传入一个下标的方式去访问entsize_list_tt
结构体里面的数据
既然得到了访问数据的方式,那么我们开始打印一下method_list_t
中的数据
此时发现并不能直接访问出来具体信息,同理我们继续在源码里查找一下method_t
这个结构体。
我们想要知道的方法的信息主要就是看sel(方法名)和imp(函数的实现)。而我们在method_t中可以看出其包含的big结构体就包含了sel和imp,同时在method_t中还发现了big()
方法可以获取到big
这个结构体。接下来我们获取一下big这个结构体
- 大端模式:
p $8.get(0).big()
- 小端模式:
p *$8.get(0).getDescription()
此时我们就获取到了实例对象的方法的信息,逐个获取method_list_t中的数据后发现多出来一个.cxx_destruct
的方法,故method_list_t的count打印出来的值是7。
.cxx_destruct
方法: 是在ARC模式下⽤于释放成员变量的,在dealloc
时调用。只有当前类拥有实例变量时这个⽅法才会出现,property⽣成的实例变量也算,且⽗类的实例变量不会导致⼦类拥有这个⽅法。
2. 属性列表(properties)获取
同样也可以通过上述方法获取properties
properties源码
属性列表获取如下:
以上我们分析得知class_rw_t
存储了当前类实例对象的属性列表property_array_t
和方法列表method_array_t
,而property_array_t
并没有存储我们声明的成员变量_hobby
和属性自动生成的_name、_age
,method_array_t
中有没有存储类方法+ (void)classMethod
,那么他们存储在哪里呢?我们下篇文章继续分析
扩展内容
1.friend关键字,通过friend关键字修饰,可以访问类的私有(private)成员和受保护(protected)成员
2.数据的大小端模式
- 大端模式:高位字节存放内存的低地址段,低位字节存放内存的高地址段。
- 小端模式:高位字节存放内存的高地址段,低位字节存放内存的低地址段。
0x12345678 0x10001 0x10002 0x10003 0x10004
大端 0x12 0x34 0x56 0x78
小端 0x78 0x56 0x34 0x12
3.entsize_list_tt
entsize_list_tt 是个模板,可以实例化出method_list_t、ivar_list_t、property_list_t三种类型。
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
Element:表示元素类型 List:表示容器类型 FlagMask:标记位