OC对象本质探索
相信大家都听说过OC对象的本质
其实就是 结构体
,但是大多数开发者不太清楚它的底层实现,接下来我们就探索一下:
Clang
-
clang
是一个由Apple主导编写,基于LLVM
的C/C++/OC
的编译器
- 主要是用于
底层编译
,将一些文件
输出成c++
文件,例如main.m
输出成main.cpp
,其目的是为了更好的观察底层的一些结构及实现的逻辑,方便理解底层原理
。
接下来我们对OC对象本质的探索就需要使用Clang编译器
。
-
首先我们创建一个Mac Comman Line Tool 空工程
- 在
main.m
中自定义一个类`LPerson ,并添加一个属性name
@interface LPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LPerson
@end
- 接下来便需要使用
Clang
将main.m
编译成main.cpp
,命令如下,这里我们使用第一种:
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 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/iPhoneSimulator13.7.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
xcode
安装的时候顺带安装了xcrun
命令,xcrun
命令在clang
的基础上进行了 一些封装,要更好用一些
使用第一种这里可能会报错'UIKit/UIKit.h' file not found
,这时改用第二种命令就可以了
- 打开编译好的
main.cpp
,找到LPerson
的定义,发现LPerson
在底层会被编译成struct 结构体
.
由此可见LPerson对象在C++底层是结构体;
- 由上面的LPerson结构体中我们还发现,其中还有个属性
NSObject_IMPL
,从代码中我们可以看出,它就是isa
,是继承自NSObject
,属于伪继承
,伪继承的方式是直接将NSObject
结构体定义为LGPerson
中的第一个属性
,意味着LGPerson 拥有
NSObject中的
所有成员变量`。
从上面源码也可以看出对象就是这样一个
结构体
,且被typedef为id
。在Foundation层id
就表示一个对象 。此时便能证明,OC对象的本质
其实就是结构体
!
- 从上面源码中我们也可以看到,
LGPerson
中的第一个属性NSObject_IVARS
等效于 NSObject中的isa
,是否记得我们用lldb
命令po
一个对象(LPerson
)的时候的返回的是<LGPerson: 0x101404a30>
,这里便是它的isa
.
关于isa
通过上面的分析我们可以了解到,在对象结构体内,有一个Class
类型的 isa
变量,他是一个指向该对象类型的指针
,在面向对象编程中,对象是由类创建的,对象可以通过isa 变量
找到自己所属的类。
Class探索
下面是Class
的源码:
从Class
源码中我们可以看到它的结构,类里面有个 super_class
,指向了类的父类
; 同时类也有一个 isa
指针,那类的 isa 指针指向了哪里呢?
对象是按照
类
所定义的各个属性和方法生产
的,类 作为对象的模板,也可看成是对象
。正如工厂里面的模子也是要专门制作模子的机器生产。元类 (meta class)
就是设计、管理类(class)
的模板。对象是 类 的实例,类是 元类 的实例。
所以类的isa
指针指向了 元类。
如果按照这个规则,聪明的你应该想到了问题了: 元类
也是对象,元类对象中也有isa
,那么元类的 isa
又指向哪里呢?总不能指向元元类吧……这样是无穷无尽的。
Objective-C语言的设计者已经考虑到了这个问题,所有元类的isa
都指向 根元类(meta Root Class)
。关于实例对象、类、元类之间的关系,苹果官方给了一张图,非常清晰的表明了三者的关系。
- 实线是
super_class
指针,虚线是isa
指针。Root class
(class) 通常是NSObject
,NSObject 是没有超类的,所以Root class
(class)的superclass
指向nil
。- 每个
Class
都有一个isa
指针指向唯一的Meta class
- Root class(meta)的 superclass 指向 Root class(class),也就是 NSObject,形成一个回路。
- 每个 Meta class 的 isa 指针都指向 Root class(meta)。
一个对象 可以通过isa
找到类,根据类的isa
和 super_class
找到 元类
与 父类
,进而直到 根元类
和 根类
,所以 对于最开始的例子LPerson *p = [[LPerson alloc] init]; ``LPerson
可以调用NSObject
的方法,在这中间isa
起到至关重要的作用。
由此我们可以得出结论:
-
Object-C 是基于类的对象系统。每一个对象都是一些类的实例;这个对象的
isa
指针指向它所属的类。- 该类描述这个 对象的数据信息 :内存分配大小(allocation size)和实例变量的类型(ivar types )与布局(layout);
- 也描述了 对象的行为 :它能够响应的选择器(selectors)和它实现的实例方法(instance methods)。
每个 Object-C 类也是一个对象,它的
isa
指针指向元类,元类是关于类对象的描述,就像类是普通实例对象的描述一样。一个
元类
是根元类的实例
;根元类是它自身的实例
。isa
指针链以一个环结束:实例指向类-指向元类-指向根元类-到自身。元类的 isa 指针并不重要,因为在现实世界中,没人会向元类对象发送消息。
isa_t
isa_t
最初出现在之前的alloc探索中,通过alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone-->initInstanceIsa
方法路径,查找到initIsa
从源码中可以看出,isa_t
是通过联合体(union)
定义的
isa_t
类型使用联合体
的原因也是基于内存优化
的考虑,这里的内存优化是指在isa指针中通过char + 位域
(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针
占用的内存大小是8字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能
从isa_t的定义中可以看出:
-
提供了两个成员,
cls
和bits
,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式通过
cls
初始化,bits无默认值
通过
bits
初始化,cls有默认值
还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 arm64(对应ios 移动端) 和 x86_64(对应macOS),以下是它们的一些宏定义,如下图所示
-
nonpointer
有两个值,表示自定义的类等,占1位-
0
:纯isa指针
-
1
:不只是类对象地址
,isa中包含了类信息
、对象的引用计数
等
-
has_assoc
表示关联对象标志位
,占1
位0
:没有关联
对象1
:存在关联
对象-
has_cxx_dtor
表示该对象是否有C++/OC的析构器
(类似于dealloc
),占1位- 如果
有析构函数
,则需要做析构逻辑
- 如果
没有
,则可以更快的释放对象
- 如果
-
shiftclx
表示存储类的指针的值
(类的地址), 即类信息-
arm64
中占33
位,开启指针优化的情况下,在arm64架构中有33位用来存储类指针 -
x86_64
中占44
位
-
magic
用于调试器判断当前对象是真的对象
还是没有初始化的空间
,占6
位-
weakly_refrenced
是 指对象是否被指向
或者曾经指向一个ARC的弱变量
- 没有弱引用的对象可以更快释放
deallocating
标志对象是是否正在释放
内存has_sidetable_rc
表示 当对象引用计数大于10
时,则需要借用该变量存储进位
-
extra_rc
(额外的引用计数) --- 表示该对象的引用计数值,实际上是引用计数值减1- 如果对象的引用计数为10,那么extra_rc为9
isa指针 位域算法
这里是macOS
环境,所以是x86_64
-
通过main中的
LGPerson
断点 -->initInstanceIsa
-->initIsa
--> 走到else中的 isa初始化
,执行lldb
命令:p newisa
,得到newisa
的详细信息
-
然后继续往下执行,走到
newisa.bits = ISA_MAGIC_VALUE
;下一行,表示为isa
的bits
成员赋值,重新执行lldb
命令p newisa
,得到的结果如下
我们发现,newsize的信息发生了变化:
- 其中
magic
是59
是由于将isa
指针地址转换为二进制
,从47
(因为前面有4个位域,共占用47位,地址是从0开始)位开始读取6
位,再转换为十进制
,如下图所示
联合体(union)
构造数据类型的方式有以下两种:
-
结构体
(struct
) -
联合体
(union
,也称为共用体
)
结构体
结构体
是指把不同的数据组合成一个整体
,其变量是共存
的,变量不管是否使用
,都会分配内存
。
缺点:所有属性都分配内存,比较
浪费内存
,假设有4个int成员,一共分配了16字节(128位)
的内存,但是在使用时,你只使用了4字节(32位)
,剩余的12字节(96位)
就是属于内存的浪费优点:存储
容量较大
,包容性强
,且成员之间不会相互影响
联合体
联合体
也是由不同的数据类型组成
,但其变量是互斥
的,所有的成员共占一段内存
。而且共用体采用了内存覆盖技术
,同一时刻只能保存一个成员的值
,如果对新的成员赋值
,就会将原来成员的值覆盖掉
缺点:
包容性弱
优点:所有成员
共用一段内存
,使内存的使用更为精细灵活,同时也节省了内存空间
两者的区别
-
内存占用情况
- 结构体的
各个成员会占用不同的内存
,互相之间没有影响
- 共用体的
所有成员占用同一段内存
,修改一个成员会影响
其余所有成员
- 结构体的
-
内存分配大小
- 结构体内存
>=
所有成员占用的内存总和
(成员之间可能会有缝隙) -
共用体
占用的内存
等于最大的成员
占用的内存
- 结构体内存