对象的本质
1.clang的了解
-
Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C轻量级编译器.源代码发布于LLVM BSD协议下.Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
-
它与GNU C语⾔规范⼏乎完全兼容(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,⽐如C函数重载(通过__attribute__((overloadable))来修饰函数),其⽬标(之⼀)就是超越GCC.
-
它主要是用于底层编译,将一些OC文件输出成C++文件,例如main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理
2. Clang操作指令
clang -rewrite-objc main.m -o main.cpp
// UIKit报错问题 -- 将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /
Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk ViewController.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 mainarm64.cpp (⼿机)
3。探索对象的本质
- 先在main函数里面写测试代码
- 通过终端,利用clang将main.m编译成 main.cpp,在终端输入以下命令
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
- 打开编译好的main-arm64.cpp,找LGPerson的定义,发LGPerson在底层会被编译成struct 结构体
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
//NSObject 的底层编译
struct NSObject_IMPL {
Class isa;
};
//LGPerson的底层编译
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
NSString *_name;
};
如下图所示
通过编译好的main-arm64.cpp我们可以看到:
-
NSObject的底层实现其实就是一个包含一个isa指针的结构体.
-
Class其实就是一个指针,指向了objc_class类型的结构体.
-
LGPerson_IMPL结构体内有三个成员变量:
-
isa 继承自父类NSObject
-
KCName
-
_name
-
对于属性name:底层编译会生成相应的setter(I_LGPerson_setName,setter方法内调用objc_setProperty方法)、getter(_I_LGPerson_name)方法,且帮我们转化为_name
通过上述分析,理解了OC对象的本质 -- 结构体,但是看到NSObject的定义,会产生一个疑问:为什么isa的类型是Class?
- alloc方法的核心之一的initInstanceIsa方法,通过查看这个方法的源码实现,我们发现,在初始化isa指针时,是通过isa_t类型初始化的,
- 而在NSObject定义中isa的类型是Class,其根本原因是由于isa 对外反馈的是类信息,为了让开发人员更加清晰明确,需要在isa返回时做了一个类型强制转换,类似于swift中的 as 的强转。。源码中isa的强转如下图所示
总结 所以从上述探索过程中可以得出:
-
OC对象的本质 其实就是 结构体
-
LGPerson中的isa是继承自NSObject中的isa
cls 与 类 的关联原理
联合体(union)
构造数据类型的方式有以下两种:
-
结构体(struct)
-
联合体(union,也称为共用体) 结构体 结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。
-
缺点:所有属性都分配内存,比较浪费内存,假设有4个int成员,一共分配了16字节的内存,但是在使用时,你只使用了4字节,剩余的12字节就是属于内存的浪费
-
优点:存储容量较大,包容性强,且成员之间不会相互影响
联合体 联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉
-
缺点:,包容性弱
-
优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
两者的区别
- 内存占用情况
结构体的各个成员会占用不同的内存,互相之间没有影响 共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
- 内存分配大小
结构体内存 >= 所有成员占用的内存总和(成员之间可能会有缝隙) 共用体占用的内存等于最大的成员占用的内存
isa的类型 isa_t
以下是isa指针的类型isa_t的定义,从定义中可以看出是通过联合体(union)定义的。
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指针中通过char + 位域 (即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能 从isa_t的定义中可以看出:
-
提供了两个成员,cls 和 bits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式
通过cls初始化,bits无默认值
通过bits初始化,cls有默认值
-
还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 arm64(对应ios 移动端) 和 x86_64(对应macOS),以下是它们的一些宏定义,如下图所示
isa原理探索
通过alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone方法路径,查找到initInstanceIsa,并进入其原理实现
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
//初始化isa
initIsa(cls, true, hasCxxDtor);
}
initIsa分析
- isa_t newisa(0)相当于初始化isa这个东西,newisa.相当于给isa赋值属性.
- SUPPORT_INDEXED_ISA适用于WatchOS,isa作为联合体具有互斥性,而cls、bits是isa的元素,所以当!nonpointer=true时对cls进行赋值操作,为false是对bits进行赋值操作(反正都是一家人,共用一块内存地址).
验证isa指针 位域(0-64)
根据前文提及的0-64位域,可以在这里通过initIsa方法证明isa指针中有这些位域(目前是处于macOS,所以使用的是x86_64).
- 首先通过main中的TCJPerson 断点 --> initInstanceIsa --> initIsa --> isa_t newisa(0)完成 isa初始化.
- 执行LLDB指令: p newisa,得到newisa的详细信息
继续往下执行,走到newisa.bits = ISA_MAGIC_VALUE;下一行,表示为isa的bits成员赋值,重新执行LLDB命令p newisa,得到的结果如下