对象的本质和isa

373 阅读7分钟

前言

对于iOSer来说,对象是天天挂在嘴边的,也许你没有对象,但是工作的时候天天跟对象打交道。所以我们今天来探寻一下我们的对象到底是什么东西。

一、对象的本质

我们使用alloc方法创建一个对象,我在上一篇文章中简单的探索了一下alloc的流程探索传送门 ,在这篇文章里 我们知道了 对象是一个与绑定的空间,那么对象的本质是什么呢?对象里面有哪些东西呢?让我们带上疑问继续探索。

1、NSObject

我们随便创建一个工程,随便写一个进入一个系统类,一直往里进最终都会发现他们继承于NSObject

截屏2021-06-16 上午11.25.37.png 如图我们发现NSObject里面就1个isa到这里似乎就没有了,但是不可能这么简单的就没了,我们需要新方法 Clang

2、Clang

Clang: a C language family frontend for LLVM

The Clang project provides a language front-end and tooling infrastructure for languages in the C language family (C, C++, Objective C/C++, OpenCL, CUDA, and RenderScript) for the LLVM project. Both a GCC-compatible compiler driver (clang) and an MSVC-compatible compiler driver (clang-cl.exe) are provided.

这是官方的解释,简单的翻译就是:ClangLLVMC语言家族前端。ClangLLVM提供C语言家族(CC++目标C/C++OpenCLCUDARenderScript)语言的语言前端和工具基础设施。同时提供了与GCC兼容的编译器驱动程序(clang)和与MSVC兼容的编译器驱动程序(clang-cl.exe)。(不要笑,使用工具翻译的)

3、用Clang探索一下对象

简单的把 .m 文件编译成 .cpp

    clang -rewrite-objc main.m -o main.cpp

我们在源文件中 main.m 中创建一个类 LGPerson 如下图所示:

截屏2021-06-16 下午2.49.26.png 然后在终端运行 编译的命令,获得如下图所示的.cpp文件:

截屏2021-06-16 下午2.40.44.png 打开文件直接搜索 LGPerson

截屏2021-06-16 下午2.50.34.png 可以发现LGPerson 是继承于结构体objc_object,而我们OC创建LGPerson的时候用的 NSObject,得出结构体objc_object就是NSObject的底层实现。

细看一下截图,通过后面2行注释确定这里是声明部分,并且我们可以看到我们的声明的之前声明的属性 testName,长的虽然不太一样但是不影响判断。我们之前声明的是属性,在这里变成了成员变量。我们再往下看一点,如图

实现.jpg

快看,我圈起来的部分是不是就是实现鸭。但是这2个关于testName的方法是什么?我们先再声明一个成员变量newTestName,如图:

截屏2021-06-16 下午3.15.07.png 再次编译,结果如图:

截屏2021-06-16 下午3.15.27.png 一目了然,并没有newTestName的相关方法,我们都知道的一个事情:成员变量和属性是一样的,差距就是属性在声明的时候会自动生成getset方法。所以这里的2个方法就是自动生成getset

在看声明里,除了我们自己声明的newTestNametestName还有一个变量存在,还是个结构体 NSObject_IVARS,搜索一下 NSObject_IMPL,发现有点多,直接搜索NSObject_IMPL {,因为我们要查看 NSObject_IMPL的内部,结果如图所示:

截屏2021-06-16 下午3.25.36.png

NSObject_IMPL中有个 isa,我们再往里看,这个Class,如图:

截屏2021-06-16 下午3.43.27.png

Classobjc_class的结构体指针。

4、总结

Clang带我们看了对象里面的东西,我们再回到objc的源码中,带着Clang的收获,直接搜索objc_object { 找到的 objc_class,如图所示:

截屏2021-06-16 下午4.20.33.png

二、isa

在上述的探索中,我们会发现一个很关键的东西 isa,它是objc_class的结构体指针,接下来需要对它进行探索。

1、位域

在程序中,某些信息存储时不需要一个完整的字节,只需要几位,为节省存储空间C语言支持“位域”的结构体。具体说就是,将一个字节分为几个段,每一段表示一个对象,这样一个字节就可以表示多个变量。位域在本质上就是一种结构类型。

首先我们创建一个结构体,结构体里的成员变量占位为1,如图: 截屏2021-06-17 上午10.56.57.png

看一下它的大小,如图

截屏2021-06-17 上午11.29.26.png

截屏2021-06-17 上午11.29.32.png

4个字节,但是我们时间上只用了 1个字节的空间。使用位域,如图:

截屏2021-06-17 上午11.32.03.png 再次运行程序,答应结果:

截屏2021-06-17 上午11.32.42.png 变成了1,是不是剩了很多空间?这就是位域。

2、联合体

联合体又叫共用体,是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的联合体,但是任何时候只能有一个成员带有值。联合体提供了一种使用相同的内存位置的有效方式。

定义方式:使用union定义。

了解个大概我们来定义一个联合体,如图:

截屏2021-06-17 下午3.22.30.png

然后断点依次赋值查看:

截屏2021-06-17 下午3.53.57.png

执行程序,断点查看:

截屏2021-06-17 下午3.56.29.png

截屏2021-06-17 下午3.56.41.png

截屏2021-06-17 下午3.56.50.png

截屏2021-06-17 下午3.57.00.png

截屏2021-06-17 下午3.57.09.png

截屏2021-06-17 下午3.57.19.png

和前面的解释一样,后赋值会对前面赋值的变量有影响。在看看大小:

截屏2021-06-17 下午4.02.01.png

8字节 double的大小。

3、isa

isaNSObject中唯一存在的变量,我们如何去查看呢?在 alloc探索的时候会有绑定 isa的方法,我们去里面看一下。

isa.png

initIsa这个方法中我们可以看到isa的类型是isa_t,那么直接进去查找吧!

截屏2021-06-17 下午4.52.35.png

是一个联合体,里面的变量有 clsbitscls是一个结构体指针所以大小为8bitsuintptr_t类型,是unsigned long也是8,所以isa的大小为8字节。clsbits互斥,ISA_BITFIELD字面意思 bit字段 ,如图:

截屏2021-06-17 下午5.20.22.png

这里分为arm64x86_642种架构,我们先看整体bit描述,以x86_64为参考,挨个分析如下

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

我们用图形表示位数:

未命名文件-2.png

再回到绑定isa的方法的地方,newisa.shiftcls = (uintptr_t)cls >> 3shiftcls存储的是类指针的值,cls是类指针,向右移3nonpointerhas_assochas_cxx_dtor的位置空出来再赋给shiftcls这样就避开了对他们的影响。运行程序我们断点查看:

截屏2021-06-18 上午1.11.18.png

进入断点后再在initIsa方法中下断点:

截屏2021-06-18 上午1.19.13.png

此时magic0说明 newisa尚未初始化,下一步:

截屏2021-06-18 上午1.22.44.png newisabits赋值为ISA_MAGIC_VALUEmagic有值了,初始化了,在 ISA_BITFIELD中看见了ISA_MAGIC_VALUE值为0x001d800000000001使用编程计算器查看:

截屏2021-06-18 上午1.30.05.png 此时4752号位置magic有值表示isa已经初始化;0号位置nonpointer1表示此isa不⽌是类对象地址,还包含了类信息、对象的引⽤计数等。继续往下:

截屏2021-06-18 上午1.38.24.png has_cxx_dtor赋值,继续:

截屏2021-06-18 上午1.40.36.png shiftcls绑定到 cls,初始化结束,我们回看person

截屏2021-06-18 上午1.48.32.pngisa为第一个指针0x001d800100008225由于iOS是小端模式,所以我们要拿到shiftcls需要先右移3把前3位去掉,再左移20(17+3)把后17位干掉,然后右移17复位,获取到 LGPerson

截屏2021-06-18 上午1.54.39.png