对象的本质
什么是.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文件了。
打开生成的.cpp文件。根据对象名称搜索,然后定位到下图的代码区域。
哇。确实和我们之前了解的一样。
对象的本质就是结构体。
这会不会是个巧合呢?我们这次添加一个特殊的成员变量来验证一下!
对象的本质就是结构体。
看到这会不会有这样的疑问呢?
struct NSObject_IMPL NSObject_IVARS这个是什么东西?- LLPerson的父类怎么是
objc_object?这个是什么东西?不应该是NSObjet吗?
带着这样的疑问,我们全局搜索
NSObject_IMPL,发现了下图的代码区域。
原来这个就是"隐藏"的成员变量
isa啊!!!它现在是Class类型。(底层层面是继承objc_object的isa。OC层面是继承NSObject的isa)
同样的我们全局搜索objc_object,发现了下图的代码区域。
发现
objc_object是个结构体。里面也有一个isa。是不是突然感觉和OC层面的NSObject一模模一样样。都是所有对象的父类,都只包含了一个isa。
我们还有一个意外发现:
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;
在变量名称后面+: 所需位数。这个就是位域的格式。
打印一下这两个结构体所需要的内存空间
联合体
当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体。
- 所有成员相对于基地址的偏移量都为0
- 此结构空间要大到足够容纳最"宽"的成员
- 对齐方式要适合其中所有的成员
union LLBigCar {
char *name;
CGFloat price;
char *author;
};
和结构体一模一样。只是关键字换成了union。那么它和结构体的区别是什么呢?
可以看出,结构体
car1的name和price变量都赋值成功了。
可以看出,联合体
car2的name最开始赋值成功了,但是当给price赋值成功后,name的值变成空了。
总结:
结构体内的变量是共存的。所有的变量都开辟内存空间,无论使用不使用。联合体内的变量是互斥的。只给最终使用的变量开辟内存空间。联合体和位域共同使用,能一定程度的节省内存空间。
isa
通过之前探索alloc底层原理,我们知道,obj->initInstanceIsa(cls, hasCxxDtor);或obj->initIsa(cls);是初始化指针和关联类的。那么我们就深入研究下这两个方法。
obj->initInstanceIsa(cls, hasCxxDtor);内部实现
obj->initIsa(cls);内部实现
发现最终都是调用:
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
可以看出,该方法里对isa进行了赋值。
这里出现了isa_t,这个是什么呢?
哦,原来是咱们上面介绍的
联合体。为什么用联合体呢?肯定是里面有多个变量,而且它们之间是互斥的关系,想要节省内存啊。那我们就来看看里面都有什么变量吧。
Class cls;要关联的类uintptr_t bits;实际上bits是一个地址一个结构体点击ISA_BITFIELD进入查看。
简单说一下各个字段的含义(基于
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的类型
总结:
isa的类型是isa_t联合体类型isa的赋值分为两种情况:- 如果没有开启指针优化,则直接关联类返回指针;
- 如果开启了指针优化,则给
isa_t联合体内的struct变量赋值
isa里存储了这么多信息,而我们通常只是想获取到类信息。类信息在isa里的位置有下面几种情况:
x86_64架构下,存储在[3 46]的位置上。前面有3位,后面有17位。arm64架构下,存储在[3 35]的位置上。前面有3位,后面有28位。 下面我们通过isa的位运算获取类信息。(我是用的真机arm64)
放张图好理解一下。