做过iOS开发的同学都应该知道我们平时编写的OC代码的底层实现都是通过C/C++实现的,所以OC的对象都是基于C/C++的数据结构实现的
1、那么OC的对象、类都是基于C/C++的什么数据结构实现的呢?
我们来建立一个项目将OC的代码转化到C/C++的代码来看一下
1)建立一个简单的项目:
2)打开终端进入项目文件夹下执行命令:$ clang -rewrite-objc main.m -o main.cpp
主意一定要生成cpp文件,如果生成c文件的话会丢失很多东西的
但是这样生成的话main.cpp文件会比较大,所以建议用xcode的一个指令进行指定在什么架构下的生成文件:$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
3)打开main-arm64.cpp文件,搜索NSObject可以查看到
所以可以看到OC的对象其实转为C/C++的结构体实现的,而结构体中只包含一个isa的指针变量,那么根据C语言的知识可以看到,isa的指针就是NSObject初始化对象的地址(其实NSObject的方法和其他的信息都存在其他的地方并不在对象的空间内,后边会说),那么从目前来看一个NSObject的对象所占用的空间就是一个指针所占的空间(64位下为8个字节,32位为4个字节)
!!!主意:其实8个字节是不严谨的,后边解释!!!
2、接下来我们创建两个NSObject的子类,Student和Person类,student继承person
2.1那么这两个类的存储空间是多少呢?
我们知道一个对象其实就是一个C/C++的结构体,那么我们将这两个类和上边一样的命令进行转化一下发现:
通过分析person实例化的对象的内存为:4 +8 =12
student实例化的对象的内存为:4 +12 =16
其实是不对的,这里会涉及内存对齐的概念,那么我们来看一下这两个对象到底占用了多少个字节呢?我们通过代码来看一下:
所以真实的占用内存的大小都是16,这是因为内存对齐的原因,对于内存对齐大家可以百度查看。你自己看一下NSObject的实例对象是多大,其实是8,所以也证明了对象的内存中只存在成员变量的值,不保存其他的信息。
2.2我们还可以对对象的占用内存细节借助xcode看一下:
1)打上断点然后View->Memory如图:
2)
由此也能看出stu的实力化对象一共所占用16个字节,_age =06 00 00 00 _no =09 00 00 00
所以再次证明:一个类的实例化对象的内存只保存属性值,不会包含其他信息
3、由上边的一系列操作可以看出一个类的实例化对象的内存只保存属性值,不会包含其他信息,但是其他的类的信息都保存在哪儿呢?而且上边还有一个细节那就是实例化对象的isa的值(指针)到底指向哪儿?
先说结论:Objective-C中的对象,简称OC对象,主要可以分为3种
1)instance对象(实例对象)就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象,instance对象在内存中存储的信息包括、isa指针、其他成员变量
2)class对象(类对象)每个类在内存中有且只有一个class对象,class对象在内存中存储的信息主要包含:isa指针、superclass指针、类的属性信息(@property)、类的对象方法信息(instance method)、类的协议信息(protocol)、类的成员变量信息(ivar)、......
3)meta-class对象(元类对象)meta-class对象和class对象的内存结构是一样的,但是用途不一样。meta-class对象在内存中存储的信息主要包含:isa指针、superclass指针、类的类方法信息(class method)、......
3.1我们先证明两个个细节:1)每个类的内存中有且只有一个class对象
2)meta_class对象在内存中也是一个但和class对象不是一个内存哦,他们都是class 类型,所以他们的结构都是一样的。内存是一个可以分析出来,因为class的对象都是一个,而获取元类的对象都是从class对象获取的,所以肯定是一样的。
获取meta_class的方法:
Class metaClass =object_getClass([NSObject class]);//获取类对象的类就是元类,但是不能[[NSObject class]class]这样获取,这样获取永远只是类对象!只能通过runtime的方法获取!
查看Class是否为meta-class:
3.2再来看一下各个对象的isa指针到底指向谁呢?
借用网上一张图:
由图看出来instance对象的isa指向class,class对象的isa指向meta_class。由此可见:oc的对象的方法调用就可以串起来了,例如之前的student的instance对象调用对象方法:
Student *stu =[[Student alloc]init];
[stu studentMethod];
stu的指针就是isa的指针,而isa指向student的class,那么其实就是student的class对象调用的studentMethod,所以就可以分析出一个类的对象方法信息其实放在class中的,分析一下:因为所有的实例化对象的方法都一样,没必要在每个对象中都开辟一份内存空间来放方法信息,因为这是对内存利用不科学的,所以放在class对象中是比较好的
接下来我们证明一下
1)新建项目:(这里面的lldb指令我就不再解释了)
很明显stu的isa的指针和stu的class对象的地址是不一样的,跟我们上边说的结论不一样,那是因为从isa指针到class的地址是要做一次位运算的,根据架构不同&的值一不一样:
如果想直接看到ISA_MASK的值的话,可以到apple官网下载runtime的源码查找,这里就不在寻找了,直接验证:
通过一次位运算之后很明显的结果是一样的,说明之前的结论是对的,我这儿是模拟器所以使用的是第二个ISA_MASK值,你如果使用真机测试的话那么请使用第一个ISA_MASK值进行运算。
接下来的Person和NSObject的验证可以自己进行。还有那幅图的superclass的指向都可以通过LLDB指令进行验证:接下来我们验证一下superclass的那条线的指向:
很明显指向跟上边的那个图是一样的
然后我们在去看一下calss对象里面都有什么,其实可以从之前从apple官网下载的代码可以找到的,这个不方便演示但是我们可以借助xcode来证明一下:我们来借助mj老师的文件MJClassInfo.h里面的mj_objc_class的结构体来将class的对象强转到结构体来窥探下class里面的东西:(这里面会涉及一点c++的知识,你只要知道上边的结论也就行了,证明作为了解):
其他的信息自己可以看下就可以看出class和metaClass对象里面的信息了如图:
(主意,在打印的属性列表里面只显示第一个属性,其他的属性需要操作指针位移运算来访问其他的属性,作为了解。还有元类对象虽然有属性列表,但是里面都是空的,因为它放在类对象里面的。还有在上边打印的里面method_list是个数组,调用的时候也是一个个往下寻找,但是如果经常被调用的时候那么这个方法会放入一个cache里面用于快速调用)
至此我们已经看到oc对象的本质了,但是我们前边有个不严谨的说法是:一个NSObject对象占用8个字节,32bit下占用4个字节。其实底层做了改变,其实是开辟了16个字节,但是实际使用的的确是8或者4个字节。怎么看一下呢?在源码里面可以看到这个函数:
所以其实是一个对象至少分配16个字节!但是实际使用为8个字节,也可以用runtime的一函数
最后的最后留个问题:给NSObject增加一个对象方法,那么[Student test]和[NSObject test],通过类调用test方法会报错么???(主意分析上边网上的那张图和一个方法的调用顺序分析一下就知道了)
ps:这个文章其实是我的学习笔记了,在小码哥学习李明杰老师的网络授课做得笔记,所以用了好多mj老师的东西,再次声明一下,感谢mj老师在it界耕耘!