OC对象的本质之:NSObject对象的内存
我们来思考一下,当我们写下下面这一行代码,以在现在主流的arm 64架构中为例,它会占用多少的内存空间?
NSObject *obj = [[NSObject alloc] init];
以上,obj是指向这个NSObject对象的指针,我们想知道这个NSObject对象占用了多少内存,首先先来了解一下NSObject在底层中是如何实现的。
NSObject的底层实现
我们都知道:我们平时编写的Objective-C代码,底层实现其实都是c/c++代码,所以其实Objective-C的面向对象都是基于c/c++的数据结构实现的。
那么:Objective-C的对象、类主要是基于c/c++的什么数据结构实现的?
稍微学过一点Objective-C的应该都能脱口而出:结构体。
我们来验证一下,将包含如下代码的main.m文件编译为c/c++的代码,看看它的实现:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return NSApplicationMain(argc, argv);
}
终端中cd到main.m文件所在的上一层目录,然后在终端中输入:
clang -rewrite-objc main.m -o main.cpp
然后回车,在main.m文件同目录下,我们就得到了main.cpp这样的c++文件。打开main.cpp文件之后发现代码多达10万行,我们可以通过指定编译的具体平台、架构来简化一下它,如指定在iOS平台(xcrun -sdk iphoneos)下的arm64架构(-arch arm64):
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
在得到的main.app文件中,我们找到定义NSObject的地方:
struct NSObject_IMPL {
Class isa;
};
这就是NSObject的底层实现,它就是一个一个结构体,这个结构体里只有一个类型为Class的成员变量isa。
那么Class是什么呢?点进去看它的内部,它其实是一个指针:
typedef struct objc_class *Class;
NSObject对象的内存大小
我们知道在arm64架构中一个指针的大小是8个字节,而NSObject_IMPL这个结构体就isa这一个成员,结构体的大小根据其成员变量来决定的, 按理来说arm64架构下,一个NSObject对象的大小就应该是8个字节,实际是不是这样的呢?
我们通过runtime库中的一个可以获取某个类的实例对象的内存大小的API:class_getInstanceSize(Class _Nullable cls),来打印一下:
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
输出:8
既然输出8,那是不是就证实了我们上面的猜想,arm64架构下,一个NSObject对象的大小就是8个字节?
别着急下结论,我们再来看一下:
malloc库中有提供可以获取指针所占内存大小的API:malloc_size(const void ptr),它接收一个类型为const void 的指针的参数
NSLog(@"zd", malloc_size((__bridge const void *)obj));
输出:16
那么到这里是不是就疑惑了,到底哪个是对的?
既然有疑惑,我们就尝试去苹果的源码中找答案,objc4源码地址:https://opensource.apple.com/tarballs/objc4/
本次分析源码版本为:objc4-818.2
我们先来看一下class_getInstanceSize(Class _Nullable cls)的实现:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
这里它调用了cls里的方法alignedInstanceSize(),从函数名我们就可以看出这是进行内存对齐原则操作的函数,我们继续追进alignedInstanceSize():
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
alignedInstanceSize()返回的是类的成员变量所占据的大小。
所以,我们应该说,一个NSObject对象创建的时候是分配了16个字节给它,只不过它真正利用起来的只有8个字节。
我们来进一步论证一下:
我们知道,alloc方法就是在通过调用allocWithZone:方法给对象分配内存,那么我们来看allocWithZone:的内部实现:
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
跟进:
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
if (fastpath(!zone)) {
obj = class_createInstance(cls, 0);
} else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
if (slowpath(!obj)) obj = _objc_callBadAllocHandler(cls);
return obj;
}
按照一般情况下的走法,此时会调用class_createInstanceFromZone,我们继续跟进去看它的实现:
/***********************************************************************
* _*class_createInstanceFromZone. Allocate an instance of the*
* specified class with the specified number of bytes for indexed
* variables, in the specified zone. The isa field is set to the
* class, C++ default constructors are called, and all other fields are zeroed.
************************************************************************ ** **/**
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
void *bytes;
size_t size;
// Can't create something for nothing
if (!cls) return nil;
// Allocate and initialize
size = cls->alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
if (zone) {
bytes = malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
bytes = calloc(1, size);
}
return objc_constructInstance(cls, bytes);
}
来到这里,一般情况下也是会走到bytes = calloc(1, size),通过调用calloc这个c语言底层的分配内存的函数来分配内存,那么这里的传参size值得我们注意。
我们往前几行来看变量size:
size_t size;
// Allocate and initialize
size = cls->alignedInstanceSize() + extraBytes;
这里先是通过调用cls里的alignedInstanceSize()函数 + 前面传进来的extraBytes给size赋值,extraBytes前面传的是0,而alignedInstanceSize()就是上面我们跟进class_getInstanceSize()时跟到的,上面的class_getInstanceSize()打印为8。
我们的重点在下一行:
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
当当当当!CoreFoundation 要求所有对象至少为 16 个字节。
至此,我们可以看出,一个NSObject对象分配内存时在来到size = cls->alignedInstanceSize() + extraBytes这里时,等于:
size = cls->alignedInstanceSize() + extraBytes; = ize = 8 + 0; = 8;
当执行完if (size < 16) size = 16,它所要分配到的内存就是16个字节了。