iOS类的底层探索(上)

2,540 阅读5分钟

前面我们探索分析了对象的底层原理。我们知道在实例对象内都有一个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);

截屏2022-04-27 下午3.53.35.png

通过打印结果看三者的内存地址相同,证明我们的类对象有且只有一个。既然我们知道类对象是Class类型,我们就先去objc源码里查找一下Class类型的声明

截屏2022-04-27 下午3.58.26.png

从源码里我们看到Calss的声明,Class类型是一个objc_class的结构体指针,也就说明了类的本质是一个objc_class的结构体

二、isa分析

再去objc源码里找一下objc_class这个结构体

截屏2022-04-27 下午4.11.37.png

在源码里我们看到objc_class继承自objc_object,之前我们探索对象的底层原理时得知对象的本质是一个objc_object的结构体,也就证明类也是一个对象,类对象的isa指针继承自objc_object这个结构体。我们知道实例对象isa指针指向它的类对象,那类对象的isa指针指向哪里呢?我们去查找一下

截屏2022-04-27 下午4.44.47.png

打印一下p对象的内存地址

截屏2022-04-29 下午1.13.43.png

通过对p对象的isa指针进行位运算 或者 & ISA_MASK掩码 得到MyClass类对象

截屏2022-04-29 下午1.53.07.png

我们在打印一下MyClass类对象的内存地址,获取类对象的isa指针

截屏2022-04-29 下午1.54.05.png

通过对 类对象的isa指针 & ISA_MASK掩码,看一下类对象的isa指针指向的内存地址

截屏2022-04-29 下午1.58.21.png

从打印结果我们发现也是显示一个MyClass,刚才我们证明了类对象有且只有一个,那么我们看一下这个MyClass是不是MyClass类对象

截屏2022-04-29 下午3.53.07.png

从打印结果来看,此时打印显示的MyClass,并不是MyClass类对象。这就是我们所说的元类。 关于元类:

1.元类对象和类对象的名字是一样的。
2.元类和类是一样的,也是一个objc_class的结构体。

证明了类对象的isa指针指向元类对象,我们继续查看元类的isa指针

截屏2022-04-29 下午4.09.03.png

同理通过元类的isa指针 & ISA_MASK掩码,发现元类的isa指针指向一个叫NSObject的东西,我们打印了NSObject类对象地址对比发现其并不是NSObject类对象(根类),此时我们猜想这是不是NSObject元类(根元类)呢,我们再打印一下NSObject的内存地址

截屏2022-04-29 下午4.15.21.png

从打印结果我们看出,元类的isa指针指向的内存地址和根元类的内存地址相同,也就是说元类的isa指针指向根元类,同时也证明根类的isa指针指向根元类,我们再继续看一下根元类的isa指针指向哪里

截屏2022-04-29 下午4.22.12.png

此时我们发现根元类的isa指针指向的内存地址和根元类的内存地址相同,也就是说根元类的isa指针指向根元类自己。此时我们可以得出isa的指向流程

截屏2022-05-05 下午3.24.09.png

三、类与元类的继承关系

知道了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);

打印结果如下

截屏2022-04-29 下午8.09.10.png

观察我们打印的地址信息,可以看到MyClass元类的父类的内存地址和NSObject元类(根元类)的内存地址相同,MySubclass元类的父类和MySubclass父类的元类的内存地址相同。也就是说元类的父类就是父类的元类。针对NSObject根类来说,根类的元类的父类就是根类自己。MySubclass的元类继承自MyClass的元类,MyClass的元类继承自NSObject元类(根元类),根元类继承自NSObject类对象,NSObject的父类为null。整理如下图所示

截屏2022-04-29 下午8.03.25.png 截屏2022-04-29 下午8.05.09.png

四、类对象的数据结构

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,我们输出一下结果

截屏2022-04-29 下午8.57.20.png

从输出的结果我们可以看出d+1,d+2,d+3与数组里对应元素的内存地址相同,也就是说我们可以通过这种方式也能获取到数组元素的内存地址。d+1,d+2,d+3这种就叫做内存平移,每次平移的大小是4个字节。这是因为我们创建的数组是int类型,int数据在内存里是占4个字节,因此我们内存平移的大小是4个字节。因此我们也是可以通过内存平移的方式取出数组里面的值

2、数据结构分析

了解了内存平移的概念之后,我们看一下objc_class这个结构体 截屏2022-04-29 下午8.24.01.png

从源码里能看出objc_class中包含类的四个重要的数据,Class ISAClass superclasscache_t cacheclass_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类对象的内存地址。

截屏2022-05-05 下午1.35.16.png 截屏2022-05-05 上午11.55.05.png

我们可以通过对MyClass类对象内存的首地址进行内存平移32个字节的方式获取到bits的内存地址,平移32个字节是因为bits前面的isa指针和superclass指针各占8个字节,cache占16个字节,总共是32个字节。打印出来的类对象的首地址为0x100008258,平移32个字节就是bits的内存首地址 0x100008278 = 0x100008258 + 0x20。从源码我们已经知道bitsclass_data_bits_t类型的结构体,那么我们对此时的地址做一个强转:

截屏2022-05-05 上午11.58.29.png

通过对强转得到的地址取值发现并不能直接获取到,那么接下来我们再去源码里面看class_data_bits_t这个类型的结构体里面是什么样子

截屏2022-05-05 下午1.43.03.png

从源码里我们找到了class_data_bits_t提供的一个data()方法,其返回值是一个class_rw_t类型的结构体,接下来我们对刚才打印得到的指针$2调用一下data()方法,打印如下

截屏2022-05-05 上午11.59.40.png

由上图可以看出我们对通过调用data()方法得到的$3取值得到了一些信息,从打印的数据里面我们并没有发现我们想要的信息。再去源码里面看一下class_rw_t这个结构体

截屏2022-05-05 上午11.36.18.png

我们从class_rw_t这个结构提源码里面发现了以上的三个方法,从方法名我们也可以看出通过方法methods()、properties()、protocols() 可以获取实例对象的方法列表、属性列表和协议列表。

1. 方法列表(methods)获取

我们先调用一下methods()方法

截屏2022-05-05 下午12.02.51.png

通过以上四步对查找我们得到method_list_t的结构体类型数据,我们可以看到里面有个count = 7的信息,这个count就是方法的个数。而MyClass的实例对象的方法我们能看到的有name、age各2个方法(get、set)加上init和instanceMethod总共6个方法,那为什么打印出来的count是7呢?我们再去源码里查找method_list_t这个结构体,看一下结构体内有没有方法可以让我们获取结构体中包含的每个方法的信息

截屏2022-05-05 下午12.32.38.png

从源码里可以发现method_list_t继承自entsize_list_tt的结构体

截屏2022-05-05 下午12.35.45.png

我们在entsize_list_tt结构体中发现了get(uint32_t i)方法,通过get(uint32_t i)方法传入一个下标的方式去访问entsize_list_tt结构体里面的数据

截屏2022-05-05 下午12.41.38.png

既然得到了访问数据的方式,那么我们开始打印一下method_list_t中的数据

截屏2022-05-05 下午12.47.52.png

此时发现并不能直接访问出来具体信息,同理我们继续在源码里查找一下method_t这个结构体。

截屏2022-05-05 下午12.53.34.png 截屏2022-05-05 下午12.56.12.png

我们想要知道的方法的信息主要就是看sel(方法名)和imp(函数的实现)。而我们在method_t中可以看出其包含的big结构体就包含了sel和imp,同时在method_t中还发现了big()方法可以获取到big这个结构体。接下来我们获取一下big这个结构体

  • 大端模式:p $8.get(0).big()
  • 小端模式:p *$8.get(0).getDescription()

截屏2022-05-05 下午2.07.38.png

此时我们就获取到了实例对象的方法的信息,逐个获取method_list_t中的数据后发现多出来一个.cxx_destruct的方法,故method_list_t的count打印出来的值是7。

截屏2022-05-05 下午2.09.26.png

  • .cxx_destruct方法: 是在ARC模式下⽤于释放成员变量的,在dealloc时调用。只有当前类拥有实例变量时这个⽅法才会出现,property⽣成的实例变量也算,且⽗类的实例变量不会导致⼦类拥有这个⽅法。
2. 属性列表(properties)获取

同样也可以通过上述方法获取properties

properties源码 截屏2022-05-05 下午2.30.18.png 截屏2022-05-05 下午2.30.55.png

属性列表获取如下:

截屏2022-05-05 下午2.33.23.png

以上我们分析得知class_rw_t存储了当前类实例对象的属性列表property_array_t和方法列表method_array_t,而property_array_t并没有存储我们声明的成员变量_hobby和属性自动生成的_name、_agemethod_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:标记位