我们知道平时编写的OC
代码,底层都是C
和C++
代码:Objective-C -> C/C++ -> 汇编语言 -> 机器语言
。所以Objective-C
对象的底层实现就是C
和C++
。
那么请思考问题:Objective-C
对象,是基于C/C++
什么类型的数据结构实现的?
答案大家都知道,结构体。因为结构体可以存放不同的数据类型。想要看清OC
的底层实现,我们可以使用下面①的命令行。但是编译后我们会发现,一个简单的文件就会编译出超过10万行的代码,这是因为clang
在编译时候,是有不同的平台区分的,比如Windows
平台,Mac
平台。所以我们可以使用②的命令指定平台,这样代码就会简化很多。
1、clang -rewrite-objc main.m -o main.cpp
2、xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
我们可以看找到一个类对象的实现,NSObject_IMPL
即NSObject implementation
。
struct NSObject_IMPL {
Class isa;
};
然后我们在定义一个类Student
然后继续执行上面的命令,看看在底层转化是否是结构体如下所示。这里是Student_IMPL
结构体的第一个成员变量就是NSObject
。这样的好处就是我定义的Student
类里拥有NSObject
的所有属性。
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
下面有个问题,为什么NSObject
的isa
是Class
类型的,通过之前的iOS开发之alloc和init分析,我们查看方法initInstanceIsa
,我们可以看到isa
的类型是isa_t
(如下图isa_t),在底层进行了强转,类似于swift
中的as
,可以查看源码(如下图强转)。
总结:
1.OC
的本质是结构体
2.子类的isa
继承自父类
isa_t的类型
在这里我们看到了一个关键词
union
(共用体/联合体),构造数据类型的方式,有两种:
1、结构体(
struct
)。
2、联合体(union
,或者叫共用体)。
结构体
结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。
- 缺点:所有属性都分配内存,比较浪费内存,假设有4个
int
成员,一共分配了16字节的内存,但是在使用时,其实使用四个字节就可以表示。那么剩下的12个字节就属于浪费。 - 优点:存储容量较大,包容性强,且成员之间不会相互影响
联合体
联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉
- 缺点:包容性弱
- 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
两者的区别
内存占用情况
- 结构体的各个成员会占用不同的内存,互相之间没有影响
- 共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
内存分配大小
- 结构体内存 >= 所有成员占用的内存总和(成员之间可能会有缝隙)
- 共用体占用的内存等于最大的成员占用的内存
我们继续回到isa_t
的分析中,我们看内部实现,发现有两个变量,cls
和bits
,上面我们讲述了共用体的特点,所以我们知道,isa
的初始化方式有两种:一、通过cls
初始化,那么bits
没有默认值。二、通过bits
初始化,cls
没有默认值。而且详细的列除了isa
的位域信息ISA_BITFIELD
(如下图)。接下来,我们详细分析这些信息所代表的含义。
1.nonpointer
- 0:纯
isa
指针 - 1:不只是类对象地址,包含了类信息,引用计数等。
2.has_assoc
表示是否有关联对象。
3.has_cxx_dtor
表示该对象是否有C++/OC
的析构函数。
- 如果有析构函数,则需要做析构逻辑。
- 如果没有,则可以更快地释放对象。
4.shiftclx
表示存储类信息。arm64
架构下33位,x86_64
架构下占44位。
5.magic
用于调试器判断当前对象是真的对象还是没有初始化的空间,占6位。
6.weakly_refrenced
,是否被弱引用,没有可以更快释放。
7.deallocating
表示对象是否正在释放内存。
8.has_sidetable_rc
表示当对象引用计数大于10时,则需要借用该变量存储进位。
9.extra_rc
(额外的引用计数) --- 导尿管表示该对象的引用计数值,实际上是引用计数值减1。如果对象的引用计数是10,那么extra_rc
为9.
验证isa指针位域信息(0-64)
上方的nonpointer
为true
,所以走下面的方法,进入第一个断点,我们进行lldb
调试,返现newisa
信息都为空,继续往下走,当走到newisa.bits = ISA_MAGIC_VALUE; define ISA_MAGIC_VALUE 0x001d800000000001ULL
后,我们在进行调试(如下图)
和上面的信息比较,我们得到了
cls = 0x001d800000000001
,在计算器中打开(如下图)111011
,然后我们把计算器转换成10进制,输入magic = 59
(如下图),同样的也是111011
,在47号位置的值就是59。有没有感觉到很神奇,666!!!所以
calloc
的意义和我们之前文章里讲述的就一模一样了,是把isa
和我们的类,关联起来。newisa.shiftcls = (uintptr_t)cls >> 3;
,我们来进一步验证这行代码。经过赋值之后,我们看到newisa
的shiftcls
被赋值成功了,注意,我们上面是没有shiftcls
的。右移三位是为了不覆盖原来的前三个位置的信息(nonpointer,has_assoc,has_cxx_dtor
)。