这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
一. 类的原理分析
类的原理分析主要是分析 isa以及 继承关系.
从 isa 开始探索 – isa走位链
首先我们先获得isa的掩码 0x00007ffffffffff8ULL,然后获得对象的地址,拿到对象的isa。

至此,我们得到了isa的掩码:0x00007ffffffffff8ULL以及对象的isa:0x011d800100008365,然后我们将这两个进行与运算,就可以得到我们的类的地址 0x0000000100008360,并且我们po 一下这个地址,证明确实是LGPerson类。

那么我们猜想一下,我们 是否可以 x/4gx 这个类的地址?我们来实验一下。

由图片可以知道,类也有相应的内存结构,那么 0x0000000100008338是不是类的 isa 呢 ?
我们把 0x0000000100008338与掩码进行一下与运算试一试。

从图片可知,两者运算后,得到的依然是LGPerson类,那么我们试着打印出两者运算后的地址

得到了两者运算后的地址为0x0000000100008338,我们从前面的运算可以知道,LGPerson类的地址为0x0000000100008360,那么为什么这个0x0000000100008338也是LGPerson类呢?是不是因为类和对象一样无限开辟,内存不止有一个类呢?我们来验证一下。

这里我们创建了多个LGPerson的class,我们打印他们的地址,看看他们的地址是否相同。

这就意味着,只有 0x100008360才是我们的LGPerson类,而0x0000000100008338不是,他是一个新的东西,这个东西就是 元类 。
元类
对象的isa是指向类,类其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类,元类是类对象的类。- 元类在代码里是不存在的,元类是
系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类。 - 元类的存在是
必需的,因为他存储了一个类的所有类方法。每个类的元类都是独一无二的,因为每个类都有一系列独特的类方法。 - 元类本身是
没有名称的,由于与类相关联,所以使用了同类名一样的名称
用MachOView(烂苹果)进行探索元类
烂苹果是分析Macho的必备工具,我们打开烂苹果,并打开Section64 (_DATA,_objc,classrefs)下的 ObjC2 References。

这里只有LGPerson,LGTeacher以及NSObject.我们再打开Section64 (_DATA_CONST,_objc,classlist).

依然只有 LGTeacher:0000001000008310以及 LGPerson:0000001000008360.
接下来我们去到 Symbol table 里面的 symbols 查找 class .


这里面多了一个东西,就是_OBJC_METACLASS_$_LGTeacher, 以及_OBJC_METACLASS_$_LGPerson,说明了我们多了一个东西就是元类,并且元类是系统进行生成和编译的.
探索到此,我们知道了对象的isa指向类,类的isa指向了元类,那么元类的isa指向哪里呢?一起来探索一番.

我们用 0x00007fff80715fe0与掩码进行与运算,并且po输出得到的地址,看看他到底是个啥。

那么我们就可以看到,元类的isa 指向的是NSObject,也就是根元类.那么根元类的isa又指向哪里呢?

由图片可知,根元类的isa指向了自己。
接下来我们继续探索NSObject.class.

由上图看到,我们的NSObject和上面的地址不一样,我们打印一下我们得到的地址.

由上图看到,NSObject的isa是0x00007fff80715fe0,我们再将这个地址与掩码进行与运算.

然后将再打印一下0x00007fff80715fe0.

我们发现,我们得到的与之前的根元类相同,并且isa也指向了自己。从对象的isa到根元类,
我们总共走了3步。
对象isa ➡️ 类isa ➡️ 元类isa➡️ 根元类 。
而从NSObject对象到根元类,则只需要2步。
根对象isa ➡️ 根类isa ➡️ 根元类
这就得出来一个非常经典的isa走位图:

继承链
类之间的继承关系
我们用一个继承自LGPerson类的LGTeacher类来探索类之间的继承关系。


由上面的图片可以得知,LGTeacher继承自 LGPerson, LGPerson继承自NSObject,而NSObject继承自Nil。
由此可以得出类之间的继承关系:
类(subClass) 继承自父类(superClass).父类(superClass)继承自根类(RootClass),此时的根类是指NSObject.根类继承自nil,所以根类即NSObject可以理解为万物起源,即无中生有.
元类之间的继承关系
探索完类之间的继承关系,我们继续探索 元类之间的继承关系 。


我们先来看LGTeacher 的元类,从输出可以看到,继承自LGPerson,那么他是继承自LGPerson类还是元类呢,我们来验证一下.

从上面的图可以看出, LGTeacher的元类 是继承自 LGPerson的元类 的。
那么LGPerson元类是继承自哪里的呢,我们来看一下:


由上图中可以看到, LGPerson的元类 是继承自 NSObject 元类 的。
继续探索一下NSObject元类是继承自哪里的:


我们先获得了 根元类(NSObject) ,然后打印出根元类的superclass,由lldb可以看出, 根元类 继承自 根类(NSObject) 。
我们最后来看一下根类NSObject的继承关系:


由输出可以看到, 根类 是继承自 null 的。
那么由上面的探索过程,我们可以得到:
元类也存在继承,元类之间的继承关系如下:
子类的元类(metal SubClass)继承自父类的元类(metal SuperClass)父类的元类(metal SuperClass)继承自根元类(Root metal Class)根元类(Root metal Class)继承于根类(Root class),此时的根类是指NSObject根类继承于null
继承链关系图:

结合isa的走位图以及继承链,我们可以证明一张来自苹果官方文档的图。

二. 类的结构分析
首先看到我们的LGPerson类的内容。

输出一下LGPerson类

由上图可以看出,LGPerson类是有内存的,那么内存里面储存的是啥呢。我们知道类的本质是objc_class.
我们在objc源码中搜索 objc_class 的定义:

我们可以看到这里有一个关于objc_class结构体的定义,但是我们注意到,下面有 OBJC2_UNAVAILABLE , 这就说明了这个结构体再objc2 中是无效的,所有这个并不是我们要找的。
继续在objc源码中寻找:

终于我们找到了objc_class结构体的定义,并且发现 objc_class 是继承自 objc_object 的,那么我们在源码中寻找objc_object,看看objc_object里面有什么成员。

有图中可以看到, objc_object 里面有 isa ,由于objc_class 继承自objc_object,所以objc_class也拥有了 isa属性 。
我们还看到 objc_class 中有三个成员: Class superclass , cache_t cache 以及 class_data_bits_t bits 。 其中 superclass 我们知道是当前的 父类 ,那么 cache 以及bits 是什么东西呢。
内存偏移
普通指针


由图片可以知道,
-
a、b都指向10,但是a、b的
地址不一样,这是一种拷贝,属于值拷贝,也称为深拷贝 -
a,b的地址之间相差 4 个字节,这取决于a、b的
类型
地址指向如下图

对象指针


由图片可以知道,
- p1、p2 是
指针,p1,p2 是 指向 [CJLPerson alloc]创建的空间地址,即内存地址.p1、p2 地址不同,指向的空间也不同。 - &p1、&p2是 指向 p1、p2对象指针的地址,这个指针就是
二级指针.

数组指针


由上图可得:
&c和&c[0]都是取首地址,即数组名等于首地址- &c 与 &c[1] 相差
4个字节,地址之间相差的字节数,主要取决于存储的数据类型 - 可以通过
首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于偏移量 * 数据类型字节数

类的地址平移
平移地址计算
由上面的探索我们知道,类有4个成员:
isa属性:继承自objc_object的isa,占8字节superclass属性:Class类型,Class是由objc_object定义的,是一个指针,占8字节cache属性:cache_t类型,不知道占多少字节bits属性:class_data_bits_t,不知道占多少字节


由上图可知道,0x00000001000083a8指向LGPerson的元类,可以证明 0x00000001000083a8 确实是isa。

由上图可知道,0x000000010036a140 是指向NSObject,可以证明 0x000000010036a140确实是superclass。
我们来看一下cache_t类型占多少字节,我们找到cache_t的结构体:

因为方法在方法区不占结构体内存以及static在全局区也不在结构体内存中,所以底下的方法以及static属性可以忽略掉。所以我们只需要看这些变量就可以。
首先是 explicit_atomic<uintptr_t> _bucketsAndMaybeMask这个属性,这个属性占多少内存呢。


我们看到explicit_atomic是一个范型,那么真正决定占用多少内存的就是uintptr_t 这个家伙,而这个家伙实际上是unsigned long 类型,unsigned long在64位中占8个字节。也就是说, explicit_atomic<uintptr_t> _bucketsAndMaybeMask这个属性是占8个字节的。

接下来是一个联合体,我们知道联合体变量是互斥的,所有的成员共占一段内存,占用的内存等于最大的成员占用的内存。

在联合体中,我们可以看到是 explicit_atomic<preopt_cache_t *> _originalPreoptCache 这个指针所占用的内存最大为8,所以这个联合体占用的内存是8.
所以我们只要平移LGPerson类的首地址平移 8 + 8 + 16 = 32 位,就可以得到bits的地址。
类的属性获取

这里我们知道这个是class_data_bits_t 的地址,所以我们可以将地址转为class_data_bits_t 指针类型,


然后我们用结构体中提供的方法data()获取数据,因为是指针,所以我们用->,如果是对象则用点语法就可以。

我们看到,这里的firstSubclass 是nil,但是我们的LGTeacher是继承自LGPerson的,那么这里为什么是nil 呢?因为我们的类加载采用的是懒加载的模式,我们访问一下这个类,LGPerson就会有firstSubclass啦。实验一下:

获取到了数据,我们就将数据打印出来。看到这里并没有我们所需要的数据,我们在进入到class_rw_t 的结构体中,并找到以下方法。

我们先获取属性数组:

然后获取List

在获取list 的ptr

还原里面的数据

获取里面的数据

这里就得到了我们所要的属性 name 和 hobby。
类的方法获取
我们再来获取类的方法列表。先获取方法数组。

接着直接获取ptr

然后还原里面的数据

接着挨个获取里面的数据

这样我们就获得所有的方法了。
这里我们注意到获取方法和获取属性的方法不一样,这是为啥呢?


这是因为property_t的结构和method_t的结构不一样。method_t的成员变量在big 中,所以我们需要额外输入big()调用big方法来获取成员变量
类的协议获取
先声明一个协议,并添加一个方法,然后为LGPerson添加这个协议。

实现一下

继续之前的流程。

这里就获取到了protocol list。

这里看到protocols()方法返回的是protocol_array_t,我们去看一下protocol_array_t是什么样子的。

看到我们最终获得一个protocol_ref_t,点进去看一下protocol_ref_t是什么。

protocol_ref_t是一个无符号长整形。
打印一下$5的值,发现是protocol_list_t类型。

看一下protocol_list_t是什么样子的。

没有打印protocol_ref_t的地方。那么该如何从protocol_ref_t里面获取信息呢?在源码里搜索一下protocol_ref_t。

看到这里有remapProtocol,而之前protocol_ref_t那里写着but unremapped,并且这里返回了protocol_t类型,那么这是不是所需要的方法呢?我们跟着这个办法,强制转换protocol_ref_t为 protocol_t *类型。
在protocol_list_t中 protocol_ref_t是list[0]:

我们获取protocol_list_t 中的 protocol_ref_t。

然后将protocol_ref_t强转为protocol_t *类型

打印一下,就得到了我们要的数据。

打印一下方法。


也是我们之前创建的方法。
类方法的位置







ivar的位置
先添加几个成员变量。

然后逐步获取:

























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)