学习对象的本质,先引入工具Clang
一、Clang
1. 什么是Clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器
2013年4月,Clang已经全面支持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下一个小更新版本)。Clang将支持其普通lambda表达式、返回类型的简化处理以及更 好的处理constexpr关键字。
Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC。
2. Clang 的使用
clang -rewrite-objc main.m -o main.cpp 把目标文件编译成c++文件
- UIKit报错问题
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.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 (手机)
二、探究对象在底层的本质是否为结构体 ?
1. LGPerson对象分析
在main.m 中自定义一个对象LGPerson,有一个属性KCName
进入main.m文件目录的终端,执行clang -rewrite-objc main.m -o main.cpp命令将main.m件编译成main.cpp文件。
打开编译好的 main.cpp 源码文件,找到LGPerson的定义,发现LGPerson在底层会被编译成 struct 结构体
结果分析:
LGPerson的底层是结构体LGPerson_IMPL中的第一个属性 其实就是isa,是继承自NSObject,属于伪继承,伪继承的方式是直接将NSObject结构体定义为LGPerson中的第一个属性,意味着LGPerson拥有NSObject中的所有成员变量。LGPerson中的第一个属性NSObject_IVARS等效于NSObject中的isa
2. NSObject分析
源码分析:
NSObject同样指向struct objc_objectNSObject结构体中,有Class类型的成员变量isa
3. Class类型的底层本质分析
isa的类型为Class,被定义为指向 objc_class的结构体指针
开发中id来表示任意对象,原因就是id被定义为指向objc_object的指针,也就指向NSObject的指针.
4. get/set方法
通过源码分析可发下,无论是getorset,都含有隐藏参数。获取当前值是是通过当前对象的首地址+ 变量的偏移值。
总结
对象的本质是结构体
LGPerson的isa是继承NSObject中的isa
NSObject中只有一个成员变量isa
三、联合体位域
学习isa之前,先了解联合体位域,为什么isa的类型isa_t是使用联合体定义
案列分析,引入一个结构体分析
struct LGCar1 {
BOOL front; // 0 1
BOOL back;
BOOL left;
BOOL right;
};
查看打印结果,当前结构体占用4个字节占用了32位,使用4个字节提现单一个功能,浪费了3倍空间.
改进如下:
// 位域
struct LGCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
改进代码使用了位域,BOOL front: 1表示占用了1位,这样LGCar2只需要4位即可,整体1个字节就可以满足需求了,0000 1111
1. 结构体struct特点分析分析
当前的结构体占用了24个字节,结构体必须是内部最大 成员的整数倍,不足的要补⻬。
| 变量 | 占用字节 | offset | min | 位置 |
|---|---|---|---|---|
| char *name | 8 | 0 | min(0, 8) | 0 ~ 7 |
| int age | 4 | 8 | min(8, 4) | 8 ~ 11 |
| double height | 8 | 16 | min(16, 8) | 16 ~ 24 |
开始阶段成员变量都是赋值为空的,执行完成,2个成员变量都能同时赋值的,处于共存状态。
2. 联合体union特点分析
// 联合体 : 互斥
union LGTeacher2 {
char *name;
int age;
double height ;
};
根据打印结果分析,联合体只能有一个被使用
第一次打印,都为 0 ,第二次打印,赋值了name字段,其他字段都不为0了,原因是其他字段不被使用了(脏数据、脏内存)
第三次打印,赋值了age,name字段为空了,联合体只能有一个被使用
3.总结分析
结构体
结构体(struct)是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。
-
优点:存储
有容乃大、包容性强,且成员之间不会相互影响 -
缺点:
struct内存空间的分配是粗放的,不管用不用,所有属性全分配内存。比较浪费,假设有4个BOOL成员,一共分配了4字节的内存,但是在使用时,你只使用了1字节,剩余的3字节就是属于内存的浪费
联合体
联合体(union)也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉
- 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
- 缺点:不够“包容”
两者的区别
-
内存占用情况
- 结构体的
各个成员会占用不同的内存,互相之间没有影响 - 共用体的
所有成员占用同一段内存,修改一个成员会影响其余所有成员
- 结构体的
-
内存分配大小
- 结构体内存
>=所有成员占用的内存总和(成员之间可能会有缝隙) 共用体占用的内存等于最大的成员占用的内存
- 结构体内存
四、isa分析
1. isa的类型isa_t
以下就是isa指针的类型isa_t的定义,从定义中可以看出是通过联合体(union)定义的
union isa_t { // 联合体
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
// 提供了cls和bits,两者是互斥关系
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa_t类型使用联合体的原因也是基于内存优化,isa指针占用的内存大小是8字节
从isa_t定义中可看出:
-
提供了两个成员变量,
cls和bits,由联合体的定义可知,这两个成员是互斥的, -
还提供了一个结构体定义的
位域,结构体成员ISA_BITFIELD,是一个宏定义,有2个版本__arm64__(iOS移动端)和__x86_64__(macOS),如下是他们的宏定义,如下图所示
-
nonpointer: 表示是否对isa指针开启指针优化0: 纯isa指针1: 不止是类对象地址,isa中包含了类信息、对象的引用计数等
-
has_assoc: 关联对象标志位0:没有关联对象1:存在关联对象
-
has_cxx_dtor: 该对象是否有C++或者Objc的析构器- 如果
有析构函数,则需要做析构逻辑 - 如果
没有,则可以更快的释放对象
- 如果
-
shiftcls: 存储类指针的值,既类的信息- arm64中占33位,开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
- x86_64中占44位
-
magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间 -
weakly_referenced: 志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。 -
deallocating: 标志对象是否正在释放内存 -
has_sidetable_rc: 当对象引用技术大于 10时,则需要借用该变量存储进位 -
extra_rc: 当表示该对象的引用计数值,实际上是引用计数值减 1,- 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
原理探索
-
探索路径
alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone,然后找到initInstanceIsa进入其实现方法。inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) { ASSERT(!cls->instancesRequireRawIsa()); ASSERT(hasCxxDtor == cls->hasCxxDtor()); initIsa(cls, true, hasCxxDtor); } -
进入
initIsa函数,主要初始化isa指针
2. isa 关联类探索
cls 与 isa 关联类的原理就是isa指针中的shiftcls存储了类信息,主要有以下几种验证方式
- 方式①:通过
initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3验证 - 方式②:
isa&ISA_MASK - 方式③:
isa位运算
方式①:通过initIsa方法
设置断点,调试程序。如下图所示:
isa_t为联合体,初始化nonpointer isa,cls属性为空,bits结构体初始化,默认都为0,
继续调试代码 到 ISA_MAGIC_VALUE后一步,可观察到第一位值位1
对isa开启指针优化,从47位到53位,可得出magic=59,表示当前对象初始化。通过计算得出59的二进制为0011 1011
继续执行代码,将类的地址 右移动3位,赋值给shiftcls,见下图。
方式②:isa&ISA_MASK
ISA_MASK是一个宏定义
-
__arm64__:ISA_MASK宏定义的值为0x0000000ffffffff8ULL -
__x86_64__:ISA_MASK宏定义的值为0x00007ffffffffff8ULL图中打印po 0x011d80010000820d & 0x0000000ffffffff8ULL
(__arm64__)和po 0x011d80010000820d & 0x00007ffffffffff8ULL(__x86_64__)结果都是LGPerson。说明isa已经关联了类
方式③:isa位运算
-
类的信息存储在
isa指针中,且isa中的shiftcls此时占44位(__x86_64__),想要读取shiftcls的44位类信息,需进过位运算,位运算是目的是只保留shiftcls的44位信息,其它位的信息都抹零,计算结果如下: -
isa
位运算过程图