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

一. 类的原理分析

类的原理分析主要是分析 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 superclasscache_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的位置

先添加几个成员变量。
在这里插入图片描述
然后逐步获取:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值