学习对象的本质,先引入工具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_object
NSObject
结构体中,有Class
类型的成员变量isa
3. Class
类型的底层本质分析
isa
的类型为Class
,被定义为指向 objc_class
的结构体指针
开发中id来表示任意对象
,原因就是id
被定义为指向objc_object
的指针,也就指向NSObject
的指针.
4. get/set
方法
通过源码分析可发下,无论是get
orset
,都含有隐藏参数。获取当前值是是通过当前对象的首地址
+ 变量的偏移值
。
总结
对象
的本质是结构体
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
位运算
过程图