iOS 底层探索篇 —— OC对象本质 & noPointerIsa|8月更文挑战

313 阅读6分钟

一. 对象的本质探索

Clang

clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器

操作代码

在这里插入图片描述

实例

在这里插入图片描述

通过这个指令,就可以把main.m 编译成 main.cpp 文件,可以更好的观察底层的一些结构及实现的逻辑,方便理解底层原理。

对象的本质

对象在底层的本质是结构体,那么是如何得到这个结论的呢。 首先我们在main.m 文件中声明一个类LGPerson,有一个属性 NSString LSName(为了避免是巧合,取一个特殊的名字)。

在这里插入图片描述

然后我们按照上文中的方法将 main.m 编译成 main.cpp 文件,然后将main.cpp 文件打开,并且搜索一下我们的类LGPerson。

在这里插入图片描述

我们可以看到LGPerson 中有两个参数 第一个参数就是isa,是继承自NSObject_IMPL,属于伪继承,伪继承的方式是直接将NSObject_IMPL结构体定义为LGPerson中的第一个属性,意味着LGPerson 拥有 NSObject_IMPL中的所有成员变量。 第二个参数就是我们的成员变量 LSName。

在这里插入图片描述

同时我们注意到,在main.cpp中,LGPerson的本质类型是 objc_object,这是为什么呢? 这是因为LGPerson继承自NSObject, 而NSObject 在底层中的实现就是objc_object,因此LGPerson的本质类型是 objc_object。

在这里插入图片描述

Class类型的本质类型

在这里插入图片描述

我们在main.cpp中寻找,找到上图可知, 得出class 是 objc_class 的结构体指针。并且我们也可以得知为什么id 不加*, 因为底层中已经加过了。

getter 和 setter

继续在main.cpp 中寻找,我们找到

在这里插入图片描述

从名字以及参数中,我们大致可以看出这个是LGPerson 的getter 和setter 方法,但是这个参数我们从来没有见到过。这些参数是隐藏参数。 为什么return 是 self + objc_ivar 呢?因为我们需要拿到内存地址才能拿到内存的值,拿到person首地址,然后拿到ivar 空间平移的量进行平移,才能获得LSName所在的地址,拿到地址才能获取里面的值。

二. 联合体位域

在这里插入图片描述

在这个结构体中,存了四个bool值,每个bool值占一个字节,根据内存对齐原则,可以得出这个struct总共占了4个字节。

在这里插入图片描述

而用sizeof可以得出,这个struct真的是占用了4个字节。显然,bool 不是 0 就是1,实际上用4位去存就可以了,用四个字节去存有点浪费内存了。

位域

这里,我们可以用位域,来指定这个成员占多少位。举个🌰。

在这里插入图片描述

这里在成员后面加了: 1,代表这个成员只占用1位,这样四个成员加起来就只占四位了。因为我们至少也需要一个字节,所以这个结构体就只占用一个字节了。我们来验证一下:

在这里插入图片描述

从输出栏可以看到,car2 现在确实只占了1个字节。

结构体(struct)所有变量是共存的

结构体是指把不同的数据组合成一个整体,其变量是共存的,结构体内存 >= 所有成员占用的内存总和。

  • 优点:是有容乃大,全面。
  • 缺点是struct内存空间的分配是粗放的,不管用不用,全分配。

联合体(union)中是各变量是互斥的

联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存,占用的内存等于最大的成员占用的内存。

  • 缺点:不够包容,采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉。
  • 优点:内存使用更加精细灵活,也节省了内存空间。

三. noPointerIsa

我们先在源码中找到isa。

1. initIsa

在这里插入图片描述

2. initIsa(cls, false, false);

在这里插入图片描述

3. isa_t

在这里插入图片描述

4. union isa_t

在这里插入图片描述

到这里,我们发现了isa_t是一个联合体位域。因为指针有8字节,而类上有很多信息可以存储,如果8个字节只是用来存指针,那么内存就会大大的浪费,所以这里定义了一个结构体位域ISA_BITFIELD,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 arm64(对应ios 移动端) 和 x86_64(对应macOS)

  • arm64在这里插入图片描述

  • x86_64

在这里插入图片描述

  • nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
  • has_assoc:关联对象标志位,0没有,1存在
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • deallocating:标志对象是否正在释放内存
  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
  • extra_rc*:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

针对两种不同平台,其isa的存储情况如图所示

在这里插入图片描述

  • 实战演练: 我们根据打印出来的结果得知,0x001d800100008275 没有占满8个字节,否则应该是0xXX1d800100008275。

在这里插入图片描述

我们假设内存是0x111d800100008275,那么和0x001d800100008275相差就是1223979098744774912。

在这里插入图片描述

更直观一点,我们可以把64 位打印出来

在这里插入图片描述

从图片可以看到,64位中还有好多位没有被使用。 那么我们如何证明类的地址和对象的地址关联了呢。

在这里插入图片描述

首先我们找到之前的结构体位域ISA_BITFIELD,如何在他的上面看到一个叫做ISA_MASK的家伙,这个是ISA的掩码。得到了ISA的掩码,我们只要拿到原来的地址,并于掩码进行与运算,就可以得到类的地址。 实际操作一下:

在这里插入图片描述

我们也可以通过位运算来进行验证。我们之前得到结构体位域ISA_BITFIELD,由于程序是运行在mac上的,所以此时的 shiftcls占44位。

  • 将isa地址左移3位:p/x 0x011d800100008275 >> 3,得到0x0023b0002000104e

  • 在将得到的0x0023b0002000104e右移20位:p/x 0x0023b0002000104e << 20 ,得到0x0002000104e00000

  • 为什么是右移20位?因为先左移了3位,相当于向左偏移了3位,而右边需要抹零的位数有17位,所以一共需要移动20位

  • 将得到的0x0002000104e00000 再左移17位:p/x 0x0002000104e00000 >> 17 得到新的0x0000000100008270, 可以看出来我们得到的和LGPerson.class 是一样的

在这里插入图片描述