这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
一、对象的本质
1、clang
- Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
- Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器
clang 命令
// 把目标编译成C++文件
clang -rewrite-objc main.m -o main.cpp
//UIKit报错
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进⾏了⼀些封装,要更好⽤⼀些
// 模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
// 真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
2、探索对象的本质
2.1、对象的本质是结构体
引入一个示例,在main.m文件中添加LRPerson类的声明和实现
打开终端,
cd至当前目录下,用如下命令将main.m编译成main.cpp文件
clang -rewrite-objc main.m -o main.cpp
main.cpp代码非常多,十万多行。全局搜索下LRPerson,发现一个结构体LRPerson_IMPL,里面正好有_name。经验证发现对象的本质是结构体
LRPerson_IMPL中除了成员变量_name,还有一个NSObject_IVARS,是一个NSObject_IMPL的结构体,搜索下NSObject_IMPL,可以看到定义如下:
所以
NSObject_IVARS就是isa
2.2、拓展
1、在LRPerson_IMPL结构体上面有这样一行代码,可以看到LRPerson是objc_object类型,这里的objc_object就是NSObject的底层实现。
typedef struct objc_object LRPerson;
2、同样可以发现在底层中Class是objc_class类型的结构体指针;我们常用的id是objc_object类型的结构体指针,这也解释了id类型可以修饰任何对象;SEL也是一个结构体指针。
typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
3、在LRPerson_IMPL结构体下面可以看到如下一段代码,这里是name属性的get/set方法
可以发现
get/set方法都有两个隐藏参数self和_cmd,也就是方法接收者和方法编号。
get/set方法是根据对象首地址和成员变量的偏移值来读取和赋值的。
二、联合体位域
先了解下联合体和位域
struct LRCar1 {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
这是一个表示方向的结构体LRCar1,BOOL类型占用1字节,这个结构体占用4字节即32位。对于一个BOOL类型,值只有0或1,LRCar1只需要占用4位就可以表示4个方向。而1字节 = 8位,所以LRCar1用1字节就可以满足需求,这里占用了4字节显然浪费了3个字节的空间。那这里如何优化呢?下面我们引入位域的概念
1、位域
struct LRCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
LRCar2和LRCar1相比,每个成员后面多了个: 1,冒号后面的数字代表成员占用多少位。使用位域后LRCar2只占用4位也就是1字节,我们打印验证一下
2、联合体
先观察下面结构体和联合体的对比,先断点打印结构体teacher1每个成员变量的赋值可以发现,结构体赋值过程中每个成员之前是互不影响的,共存在这个结构体中。
而我们发现联合体
teacher2在赋值过程中,成员变量间会受到影响,呈互斥状态。
分别打印一下结构体
teacher1和联合体teacher2各成员变量的地址。可以发现结构体成员变量有自己的内存空间,而联合体成员变量共用同一个内存空间,所以联合体成员变量间互斥。
总结
结构体(struct)中所有变量是“共存”的——优点是“有容乃⼤”,全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全分配。联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”;但优点是内存使⽤更为精细灵活,也节省了内存空间
三、nonPointerIsa分析
在第一篇alloc探索中,我们得知initInstanceIsa这一步是初始化指针关联类,跟进去来到initIsa,这里就是初始化isa的方法,isa是isa_t类型。
先看下
isa_t
我们发现
isa_t是一个联合体,包含bits、cls,还有一个结构体ISA_BITFIELD。
普遍在表现一个类地址过程中,经常会出现一个有意思的名词叫nonPointerIsa。平时说的类也是一个对象,是个指针。我们发现类上可以有很多信息可以存储,指针是8字节即64位,如果只存一个指针,未免有点浪费。是否可以优化一下呢?在类中除了指针还可以有其他东西,比如是否正在释放、引用计数、weak、关联对象、析构函数等等,这就出现了nonPointerIsa,nonPointerIsa不再是一个简简单单的指针。如何验证这些呢?我们需要去看ISA_BITFIELD
ISA_BITFIELD针对不同平台arm64和x86位域设置不同
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推导class
先
x/4gx打印出p,再p/x打印出LRPerson.class。我们发现打印出的isa和类并不一致。此时isa应该和类已经关联起来,但这里并没有看出什么来。原因是这里没有& ISA_MASK,isa与上掩码才能得到类。是因为isa中不止是类的信息,还有很多其他信息,需要通过掩码来获取类的信息
五、isa位运算
假使我们并不知道掩码,根据上面分析已经清楚isa的位域分布结构,类信息在shiftcls中,那么可以根据位运算来获取shiftcls。
以
x86_64为例,shiftcls占用44位,前边有3位,后边有17(6+1+1+1+8)位
isa先右移3位,清空shiftcls前边3位- 再左移
20位,清空shiftcls后边17位以及复原刚右移的3位 - 最后再右移
17位,复原到最初的位置,最后得到类的信息。可以看到打印出LRPerson