OC对象的本质之:NSObject对象的内存

2,419 阅读4分钟

我们来思考一下,当我们写下下面这一行代码,以在现在主流的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源码地址:opensource.apple.com/tarballs/ob…

本次分析源码版本为: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);

}

跟进:

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

按照一般情况下的走法,此时会调用class_createInstanceFromZone,我们继续跟进去看它的实现:

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());
    
    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

来到这里,一般情况下也是会走到obj = (id)calloc(1, size);,通过调用calloc这个c语言底层的分配内存的函数来分配内存,那么这里的传参size值得我们注意。

我们往前几行来看变量size:

size = cls->instanceSize(extraBytes);

这里先是通过调用alignedInstanceSize()函数 + 前面传进来的extraBytes给size赋值,extraBytes前面传的是0,而alignedInstanceSize()就是上面我们跟进class_getInstanceSize()时跟到的,上面的class_getInstanceSize()打印为8。

inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }
        
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

我们的重点在下一行:

// 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; = size = 8 + 0; = 8;

当执行完if (size < 16) size = 16,它所要分配到的内存就是16个字节了。