Objective-C 计算对象内存大小

系统提供了两种计算对象实例内存大小的方式

  1. 通过 runtime 提供的 class_getInstanceSize API
  2. 通过底层的 malloc_size 方法。

但是这两个 API 可能会返回不同的结果,看下下面的代码预期会发生什么

size_t size = class_getInstanceSize([NSObject class]);
size_t msize = malloc_size((__bridge const void *)([[NSObject alloc] init]));
printf("%zu\n",size);
printf("%zu\n", msize);

我们知道 NSObject 的成员变量仅仅是包含一个8字节的 isa 指针,所以预期结果是,两个 API 返回的结果都是 8 字节。事实上 malloc_size API 返回的结果是 16 字节。

为什么?

我们看下 class_getInstanceSize API 的实现

#define WORD_MASK 7UL
size_t class_getInstanceSize(Class cls) {
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
uint32_t unalignedInstanceSize() const {
    return data()->ro()->instanceSize;
}

先获取未对齐的实例大小(OC 编译后实例的内存大小布局就已经确定了),然后将实例大小进行字对齐操作(64 位系统的按照 8 字节对齐),最终得到的大小就是 class_getInstanceSize 的值。这种计算方式其实并不需要系统真正的分配对象,而是在编译期就能拿到编译后的类实例所占用的内存大小。

在上面的例子里面 isa 是 8 个字节,对齐后依然是 8 字节,所以 class_getInstanceSize 返回 8 字节没有问题。

再看 malloc_size 为什么返回 16 字节?

malloc_size API 的介绍如下,它是返回系统分配给指定指针指向内存的内存块的大小。

The malloc_size() function returns the size of the memory block that backs the allocation pointed to by ptr. The memory block size is always at least as large as the allocation it backs, and may be larger.

但是在 iOS 在分配内存的时候,最小分配的颗粒度是 16 字节,参考官方文档 Tips for Allocating Memory (opens new window)

When allocating any small blocks of memory, remember that the granularity for blocks allocated by the malloc library is 16 bytes. Thus, the smallest block of memory you can allocate is 16 bytes and any blocks larger than that are a multiple of 16. For example, if you call malloc and ask for 4 bytes, it returns a block whose size is 16 bytes; if you request 24 bytes, it returns a block whose size is 32 bytes. Because of this granularity, you should design your data structures carefully and try to make them multiples of 16 bytes whenever possible.

任何小于 16 字节的内存会按照最小颗粒度 16 字节进行大小分配。所以这就是在运行时分配内存的时候,尽管 NSObject 实例只占用 8 个字节,但是也要按照 16 字节分配的原因。

下面这个例子中预期的输出结果是什么?

@interface Person : NSObject
@property(nonatomic) NSString *name;
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        size_t isize = class_getInstanceSize([Person class]);
        NSLog(@"class_getInstanceSize %zu",isize);
				Person *person = [[Person alloc] init];
        size_t msize = malloc_size((__bridge const void *)(person));
        NSLog(@"malloc_size %zu",msize);
    }
    return 0;
}

通过 class_getInstanceSize API 计算的时候,还是按照编译后的成员变量大小占用来计算, Person 类有 isa 指针成员变量以及 NSString 指针。两个指针分别占用 8 个字节,所以输出结果是 16 字节。

再看 malloc_size API 输出的结果也是 16 个字节。可能会疑惑刚从说的分配内存不是按照最小颗粒度是 16 字节分配吗?为啥一个 isa 指针分配是 16 字节,多了一个 NSString 指针还是 16 字节。

我认为这里涉及到内存对齐的概念,即两个指针都占用 8 个字节,而最小颗粒度 16 字节能容纳下来两个指针的大小。所以系统没有必要再额外分配多余的内存给成员变量。

如果将 Person 中的 NSString 替换为占用 8 字节的 int 类型,结果如何呢?

@interface Person : NSObject
@property(nonatomic) int a;
@end

通过 class_getInstanceSize API 计算得到的结果是 16 字节,这里依然是考虑了内存对齐,如果不考虑内存对齐,Person 实例实际占用的内存是 isa(8字节)+ int(4字节) = 12 字节大小,但是因为内存对齐的缘故,实际计算结果是 16 字节。

所以其实不论是 class_getInstanceSize 还是 malloc_size 在实际计算内存的时候都会考虑到内存对齐的操作。

参考地址:

  1. iOS底层探索之内存对齐和calloc (opens new window)
  2. Tips for Allocating Memory (opens new window)
  3. malloc_size API (opens new window)
  4. Swift 内存模型 (opens new window)