iOS底层-对象的本质&nonPointerIsa

347 阅读6分钟

对象的本质

什么是.cpp文件

我们想要理解对象的本质,就需要知道对象在底层是如何被定义和实现的。想要知道对象在底层的实现,就需要探索.cpp文件。
.cpp文件被称作C++源文件,里面放的都是程序实现的源代码。

Clang

那么我们如何得到自己编写的对象对应的.cpp文件呢?Clang就是干这事的。

  • Clang是一个C语言、C++、Objective-C语言的轻量级编译器,是由Apple主导编写的。
  • Clang主要用于把源文件编译成底层文件,比如把main.m文件编译成main.cpp、main.o或者可执行文件。便于观察探索底层的逻辑结构。 生成.cpp文件。Xcode安装的时候顺带安装了xcrun命令,xcrun命令是在Clang的基础上进行了一些封装,要更好用一些。
  • xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 你要编译的对象名称.m -o 你要编译的对象名称.cpp 真机
  • xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc 你要编译的对象名称.m -o 你要编译的对象名称.cpp 模拟器 首先在终端cd进入你要编译的对象名称所在的文件夹,然后根据自己的需求执行上面的命令。之后就能得到.cpp文件了。

51.png

52.png

打开生成的.cpp文件。根据对象名称搜索,然后定位到下图的代码区域。

53.png 哇。确实和我们之前了解的一样。对象的本质就是结构体
这会不会是个巧合呢?我们这次添加一个特殊的成员变量来验证一下!

84.png

85.png

  • 对象的本质就是结构体

看到这会不会有这样的疑问呢?

  • struct NSObject_IMPL NSObject_IVARS 这个是什么东西?
  • LLPerson的父类怎么是objc_object?这个是什么东西?不应该是NSObjet吗?

WechatIMG88.png 带着这样的疑问,我们全局搜索NSObject_IMPL,发现了下图的代码区域。

WechatIMG89.png 原来这个就是"隐藏"的成员变量isa啊!!!它现在是Class类型。(底层层面是继承objc_object的isa。OC层面是继承NSObject的isa)

同样的我们全局搜索objc_object,发现了下图的代码区域。

WechatIMG90.png 发现objc_object是个结构体。里面也有一个isa。是不是突然感觉和OC层面的NSObject一模模一样样。都是所有对象的父类,都只包含了一个isa。

我们还有一个意外发现:

WechatIMG91.png Class是结构体objc_class指针类型的别名,id是结构体objc_object指针类型的别名。
是不是突然理解为什么平时开发的时候可以这样写:(id)(LLPerson对象)==(LLPerson *)(LLPerson对象)。

对象的本质

总结:

  • 对象的本质就是结构体
  • 底层objc_object是基类。OC层NSObject是基类。

nonPointerIsa

结构体、联合体、位域

了解探索nonPointerIsa之前,我们先要掌握结构体联合体位域的相关知识点。

结构体

关于结构体之前有相关的讲解。这里就不再啰嗦。

位域

有些信息在存储时,并不需要占用一个完整的字节。例如在存放一个开关变量时,只有0和1两种状态,用一位二进位即可。位域的作用是限定数据的位数,节约内存

struct LLCar {
    BOOL front;
    BOOL back;
    BOOL left;
    BOOL right;
}llCar1;

上面的llCar1要占用4个字节的内存空间。也就是32位。而从实际出发,其实只需要4位就可以表达llCar1,造成了不必要的内存浪费。
0000 0000 0000 0000 0000 0000 0000 0000
位域就是为了解决这种内存空间浪费的。

struct LLBigCar {
    BOOL front : 1;
    BOOL back  : 1;
    BOOL left  : 1;
    BOOL right : 1;
}llCar2;

在变量名称后面+: 所需位数。这个就是位域的格式。
打印一下这两个结构体所需要的内存空间

WechatIMG42.png

联合体

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体。

  • 所有成员相对于基地址的偏移量都为0
  • 此结构空间要大到足够容纳最"宽"的成员
  • 对齐方式要适合其中所有的成员
union LLBigCar {
    char *name;
    CGFloat price;
    char *author;
};

和结构体一模一样。只是关键字换成了union。那么它和结构体的区别是什么呢?

WechatIMG44.png 可以看出,结构体car1nameprice变量都赋值成功了。

WechatIMG45.png 可以看出,联合体car2name最开始赋值成功了,但是当给price赋值成功后,name的值变成空了。

总结:

  • 结构体内的变量是共存的。所有的变量都开辟内存空间,无论使用不使用。
  • 联合体内的变量是互斥的。只给最终使用的变量开辟内存空间。
  • 联合体位域共同使用,能一定程度的节省内存空间。

isa

通过之前探索alloc底层原理,我们知道,obj->initInstanceIsa(cls, hasCxxDtor);obj->initIsa(cls);是初始化指针和关联类的。那么我们就深入研究下这两个方法。

WechatIMG96.png obj->initInstanceIsa(cls, hasCxxDtor);内部实现

WechatIMG97.png obj->initIsa(cls);内部实现

WechatIMG98.png 发现最终都是调用:

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)

WechatIMG99.png 可以看出,该方法里对isa进行了赋值。
这里出现了isa_t,这个是什么呢?

WechatIMG100.png 哦,原来是咱们上面介绍的联合体。为什么用联合体呢?肯定是里面有多个变量,而且它们之间是互斥的关系,想要节省内存啊。那我们就来看看里面都有什么变量吧。

  • Class cls;要关联的类
  • uintptr_t bits;实际上bits是一个地址
  • 一个结构体 点击ISA_BITFIELD进入查看。

WechatIMG101.png 简单说一下各个字段的含义(基于x86_64架构):

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

回到isa的赋值方法里,查看一下isa的类型

WechatIMG102.png

总结:

  • isa的类型是isa_t联合体类型
  • isa的赋值分为两种情况:
    • 如果没有开启指针优化,则直接关联类返回指针;
    • 如果开启了指针优化,则给isa_t联合体内的struct变量赋值

isa里存储了这么多信息,而我们通常只是想获取到类信息类信息isa里的位置有下面几种情况:

  • x86_64架构下,存储在[3 46]的位置上。前面有3位,后面有17位。
  • arm64架构下,存储在[3 35]的位置上。前面有3位,后面有28位。 下面我们通过isa的位运算获取类信息。(我是用的真机arm64

WechatIMG104.png 放张图好理解一下。

WechatIMG105.png