iOS 底层探索篇 —— 类的原理分析-上

这是我参与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

在这里插入图片描述

这里只有LGPersonLGTeacher以及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继承自 LGPersonLGPerson继承自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的位置

先添加几个成员变量。

在这里插入图片描述

然后逐步获取:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享