这是我参与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