作为一名软件开发工程师,我们都了解,程序最终要运行在终端,需要经过预处理、编译、装载、链接几个过程。正因如此,想了解NSObject的本质问题,我们就需要知道在编译阶段都做了些什么事情。
Object-C语言属于C语言分支,很自然我们会想到,它的编译过程会不会是先转换成C/C++,然后再转换为汇编语言,继而形成最终的机器码010101,经过链接后,开始真正的运行起来。如图所示:
那么怎么来进行验证呢?首先我们新建一个命令行工程,创建一个NSObject对象:
NSObject *obj = [[NSObject alloc]init];
利用Clang编译器前端,将我们所编写的Object-C 语言转换为C++。
打开终端,在终端输入命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.arm64.cpp
该条指令的意思是利用 Xcode 的clang编译器,将main.m文件,编译为支持iOS 64位架构的C++文件,导出文件名为main.arm64.cpp。
打开生成的C++文件,搜索 NSObject_Impl,我们会看到如下代码:
看名字:NSObject_IMPL,我们可以理解为NSObject_implection,及NSObject类的实现部分,对比NSObject.h中对于NSObject的定义:
此时我们可以发现,@interface 类经编译器编译后,变成了C++中的结构体类型,也就是说NSObject的本质就是C++中的结构体。
明白了NSObject是通过C++的结构体进行实现的,下一步的工作就是探究一个NSObject对象,在内存中占用多少内存了。
结构体中只有一个Class类型的isa成员,那么Class又是什么结构呢,继续深究我们发现:
ISA其实是一个指向结构体的指针,在64位架构中,一个指针占有8个字节的内存大小,因为结构体NSObject中只有一个成员isa,也就是说ISA的地址就是NSObject的地址,NSObject占用8个字节的存储空间。为了验证我们的猜测是否正确,可以通过runtime中提供的class_getInstanceSize来进行验证:
结果和我们猜测的一样,输出size = 8。
细心的朋友已经发行在控制台中还有一个打印结果 allocSize = 16。
那这个又该如何解释呢?
我们通过runtime获取的是实际占用的空间大小,通过malloc_size函数获取的是系统分配的内存大小,也就是说系统给NSObject对象分配了16个字节的空间,但是结构体中只有一个isa成员,在64位环境下,只占用了前面8个字节大小。
同时我们也可以利用Xcode的debug工具来进行验证。
在刚才的断点处,我们看到NSObject的地址为0x100402f90,打开Xcode的viewMemory调试器,步骤为Debug->DebugWorkFlow->ViewMemory,在address栏输入刚才的地址,看到如下结果:
注意:我们在这里看到左边的地址是十进制表示形式,右边显示的是16进制表示法,0x100402f90对应的十进制值为:
系统分配了一段连续的内存空间,1个16进制数字代表4个2进制位,2个16进制刚好表示1个字节。从图中我们可以看出前8个字节已经被占用,也就是isa变量的地址,后8个字节为0,为空。从这里也可以间接证明系统为NSObject对象分配了16个字节,实际只用了8个字节的内存大小。
总结:1.NSObject对象的本质是结构体
2.在64位环境下,系统为NSObject对象分配了16个字节,因为结构体中只有一个指向结构体的指针isa成员,只占用了8个字节的大小。
3.通过runtime中的 class_getInstanceSize() 函数获取到的是对象实际占用的地址
通过maclloc函数:malloc_size()获取到的是系统为对象分配的地址。
如果还有不清楚的地方,欢迎探讨。
参考内容:小码哥底层原理:NSObject对象的本质