前言
对于iOSer来说,对象是天天挂在嘴边的,也许你没有对象,但是工作的时候天天跟对象打交道。所以我们今天来探寻一下我们的对象到底是什么东西。
一、对象的本质
我们使用alloc方法创建一个对象,我在上一篇文章中简单的探索了一下alloc的流程探索传送门 ,在这篇文章里 我们知道了 对象是一个与类绑定的空间,那么对象的本质是什么呢?对象里面有哪些东西呢?让我们带上疑问继续探索。
1、NSObject
我们随便创建一个工程,随便写一个进入一个系统类,一直往里进最终都会发现他们继承于NSObject
如图我们发现
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.
这是官方的解释,简单的翻译就是:Clang 是LLVM的C语言家族前端。Clang为LLVM提供C语言家族(C、C++、目标C/C++ 、OpenCL、CUDA和RenderScript)语言的语言前端和工具基础设施。同时提供了与GCC兼容的编译器驱动程序(clang)和与MSVC兼容的编译器驱动程序(clang-cl.exe)。(不要笑,使用工具翻译的)
3、用Clang探索一下对象
简单的把 .m 文件编译成 .cpp
clang -rewrite-objc main.m -o main.cpp
我们在源文件中 main.m 中创建一个类 LGPerson 如下图所示:
然后在终端运行 编译的命令,获得如下图所示的
.cpp文件:
打开文件直接搜索
LGPerson
可以发现
LGPerson 是继承于结构体objc_object,而我们OC创建LGPerson的时候用的 NSObject,得出结构体objc_object就是NSObject的底层实现。
细看一下截图,通过后面2行注释确定这里是声明部分,并且我们可以看到我们的声明的之前声明的属性 testName,长的虽然不太一样但是不影响判断。我们之前声明的是属性,在这里变成了成员变量。我们再往下看一点,如图
快看,我圈起来的部分是不是就是实现鸭。但是这2个关于testName的方法是什么?我们先再声明一个成员变量newTestName,如图:
再次编译,结果如图:
一目了然,并没有
newTestName的相关方法,我们都知道的一个事情:成员变量和属性是一样的,差距就是属性在声明的时候会自动生成get和set方法。所以这里的2个方法就是自动生成get和set。
在看声明里,除了我们自己声明的newTestName和testName还有一个变量存在,还是个结构体 NSObject_IVARS,搜索一下 NSObject_IMPL,发现有点多,直接搜索NSObject_IMPL {,因为我们要查看 NSObject_IMPL的内部,结果如图所示:
NSObject_IMPL中有个 isa,我们再往里看,这个Class,如图:
Class是objc_class的结构体指针。
4、总结
Clang带我们看了对象里面的东西,我们再回到objc的源码中,带着Clang的收获,直接搜索objc_object { 找到的 objc_class,如图所示:
二、isa
在上述的探索中,我们会发现一个很关键的东西 isa,它是objc_class的结构体指针,接下来需要对它进行探索。
1、位域
在程序中,某些信息存储时不需要一个完整的字节,只需要几位,为节省存储空间C语言支持“位域”的结构体。具体说就是,将一个字节分为几个段,每一段表示一个对象,这样一个字节就可以表示多个变量。位域在本质上就是一种结构类型。
首先我们创建一个结构体,结构体里的成员变量占位为1,如图:
看一下它的大小,如图
4个字节,但是我们时间上只用了 1个字节的空间。使用位域,如图:
再次运行程序,答应结果:
变成了
1,是不是剩了很多空间?这就是位域。
2、联合体
联合体又叫共用体,是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的联合体,但是任何时候只能有一个成员带有值。联合体提供了一种使用相同的内存位置的有效方式。
定义方式:使用union定义。
了解个大概我们来定义一个联合体,如图:
然后断点依次赋值查看:
执行程序,断点查看:
和前面的解释一样,后赋值会对前面赋值的变量有影响。在看看大小:
8字节 double的大小。
3、isa
isa是NSObject中唯一存在的变量,我们如何去查看呢?在 alloc探索的时候会有绑定 isa的方法,我们去里面看一下。
在initIsa这个方法中我们可以看到isa的类型是isa_t,那么直接进去查找吧!
是一个联合体,里面的变量有 cls和 bits,cls是一个结构体指针所以大小为8,bits是 uintptr_t类型,是unsigned long也是8,所以isa的大小为8字节。cls和 bits互斥,ISA_BITFIELD字面意思 bit字段 ,如图:
这里分为arm64和x86_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。
我们用图形表示位数:
再回到绑定isa的方法的地方,newisa.shiftcls = (uintptr_t)cls >> 3;shiftcls存储的是类指针的值,cls是类指针,向右移3将nonpointer、has_assoc和has_cxx_dtor的位置空出来再赋给shiftcls这样就避开了对他们的影响。运行程序我们断点查看:
进入断点后再在initIsa方法中下断点:
此时magic为 0说明 newisa尚未初始化,下一步:
newisa的bits赋值为ISA_MAGIC_VALUE后magic有值了,初始化了,在 ISA_BITFIELD中看见了ISA_MAGIC_VALUE值为0x001d800000000001使用编程计算器查看:
此时
47至52号位置magic有值表示isa已经初始化;0号位置nonpointer为1表示此isa不⽌是类对象地址,还包含了类信息、对象的引⽤计数等。继续往下:
has_cxx_dtor赋值,继续:
shiftcls绑定到 cls,初始化结束,我们回看person:
取
isa为第一个指针0x001d800100008225由于iOS是小端模式,所以我们要拿到shiftcls需要先右移3把前3位去掉,再左移20(17+3)把后17位干掉,然后右移17复位,获取到 LGPerson: