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个字节了。

作者:Grb3e

%s 个评论

要回复文章请先登录注册