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

3,577 阅读6分钟

之前分享过NSObject对象的内存大小,但是在我们日常开发中使用的类一般都是继承于NSObject,那么这些类它们的内存大小又是如何分配的呢?

先来一道开胃菜:

我定义了一个继承于NSObjectPerson类如下:

@interface Person : NSObject
{
    @public
    int _age;
}
@end

那么,我们来思考一下,Person创建出来的对象,会占用多少的内存呢?

Person *per = [[Person alloc] init];

这次我们不卖关子了,直接使用malloc库中的malloc_size(const void *ptr)方法来看一下:

NSLog(@"size = %zd", malloc_size((__bridge const void *)per));
输出:size = 16

我相信绝大多数人都能答对,如果你对此有疑问,我们还是先来看一下Person编译为c++代码后的实现:

struct Person_IMPL {
   struct NSObject_IMPL NSObject_IVARS;
   int _age;
};

Person继承于NSObject,在NSObject对象的内存大小中,我们已经知道结构体NSObject_IMPL实际上就是NSObject的底层实现,而NSObject_IMPL内也只有一个成员变量isa

struct NSObject_IMPL {
    Class isa;
};

我们知道在arm64架构中一个指针的大小是8个字节,而NSObject_IMPL这个结构体就isa这一个成员,所以NSObject_IMPL也就是8个字节。

这时候可能有人就会有疑惑了:说在NSObject对象的内存大小中你又说NSObject对象占了16个字节的内存?这里怎么又说NSObject_IMPL是8个字节?

可能有点儿绕,但这里我们要搞清楚的是:NSObject对象的实际所需大小确实只是8个字节,只是它在alloc的过程中,由于CoreFoundation的规定而至少分配了16个字节,但是当NSObject_IMPL在这里作为Person的成员变量,它就是实际的8个字节的大小。

所以Person对象per的成员变量NSObject_IVARS就是按8个字节来计算的,最终会输出:

size = 16

喝点高汤:

此时我们再定义一个Student类,使它继承于Person

@interface Student : Person
{
    @public
    int _no;
}
@end

那么,还是照惯例:我们来思考一下,Student创建出来的对象,会占用多少的内存呢? 不卖关子,直接看结果:

Student *stu = [[Student alloc] init];
NSLog(@"student size = %zd", malloc_size((__bridge const void *)stu));
输出:16

???此时屏幕前的你是否是黑人问号脸? 有了之前的经验,我们猜也能猜出来Student是怎样定义的:

struct Student_IMPL {
   struct Person_IMPL Person_IVARS;
   int _no;
};

那么,根据结构体的内存对齐原则,Person_IMPL的大小已经是16个字节了,再加上自己的成员变量_no,应该要大于16个字节才对啊?! 这是因为:虽然Person_IMPL占用了16个字节的大小,而系统给Student分配内存时也确实有16个字节是属于结构体Person_IMPL的 但Person_IMPL的成员变量实际占用的也就是8 + 4 = 12个字节,有四个字节是空着的,所以为了避免内存浪费,编译器发现还空着4个字节是可以接着放_no的,就会放上去,而不是另外再去占用更多的内存空间。 为了证明我们的猜想,我们看一下Student对象的内存地址:

Student *stu = [[Student alloc] init];
stu->_age = 8;
stu->_no = 5;
po stu
输出:<Student: 0x600000008040>

复制此内存地址,然后通过Xcode - Debug - Debug Workflow - View Memory,在地址栏粘贴Student对象的地址然后回车:

image.png 这个页面显示的是16进制的,一个16进制位代表4个二进制位,那两个16进制位就代表8个二进制位,8位就是一个字节,所以我们从头数,49是第一个字节,82是第二个字节......数到16个,你就会发现我们给_age_no的赋值。

或是通过lldb的指令来查看:

image.png

虽然这些方式好像都不是很严谨,但也稍微能从侧面证实一下malloc_size(const void *ptr)给出的结果,所以Student的内存应该就是这样的: image.png

开始上正菜:

我们给Person添加一个成员变量_name

@interface Person : NSObject
{
    @public
    int _age;
    NSString *_name;
}
@end

那么这时候Person创建出来的对象,会占用多少的内存呢?

struct Person_IMPL {
   struct NSObject_IMPL NSObject_IVARS;//8个字节
   int _age;//4个字节
   NSString *_name;//8个字节
};

可能有人会这么想: 8 + 4 + 8 = 20,再根据结构体的内存对齐原则,答案应该是24? 对不对呢?我们直接看结果:

Person *per = [[Person alloc] init];
NSLog(@"person instanceSize = %zd", class_getInstanceSize([Person class]));
NSLog(@"person size = %zd", malloc_size((__bridge const void *)per));
输出:person instanceSize = 24
     person size = 32

也就是说,结构体Person_IMPL对齐后所需要的内存大小只是24个字节,但是编译器最终给它分配的却是32个字节,这个结果在不在你的意料之中呢?

我们还是尝试去苹果的源码中找答案,objc4源码地址:opensource.apple.com/tarballs/ob…

本次分析源码版本为:objc4-818.2

还是来到_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)

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);
}

我们来看这一句:size = cls->instanceSize(extraBytes);

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;
    }

之前我们分析NSObject对象的内存大小的时候也说到这里了,extraBytes前面传进来的是0,而alignedInstanceSize()内部就是内存对齐的操作。而class_getInstanceSize(Class  _Nullable __unsafe_unretained cls)内部调用的其实也是alignedInstanceSize()

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

所以此处我们可以知道:

size = cls->alignedInstanceSize() + extraBytes;
其实就是:size = 24 + 0;

所以,当执行到obj = (id)calloc(1, size);的时候,传的size就是24。 那输出32又是为什么呢? 那我们就要继续追踪跟进calloc函数了。

image.png

我们在libmalloc库中找到calloc(size_t __count, size_t __size)的实现:

void *
calloc(size_t num_items. size_t size)
{
   void *retval;
   retval = malloc_zone_calloc(default_zone, num_items, size);
   if (retval == NULL) {
      errno = ENOMEM;
   }
   return retval;
}

跟进malloc_zone_calloc(default_zone, num_items, size)

_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
		malloc_zone_options_t mzo)
{
	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
 
	void *ptr;
	if (malloc_check_start) {
		internal_check();
	}
	ptr = zone->calloc(zone, num_items, size);
 
	if (os_unlikely(malloc_logger)) {
		malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
				(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
	}
 
	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
	if (os_unlikely(ptr == NULL)) {
		malloc_set_errno_fast(mzo, ENOMEM);
	}
	return ptr;
}

跟进ptr = zone->calloc(zone, num_items, size);,发现点不进去,借助lldb断点发现跟到了default_zone_calloc

static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
	zone = runtime_default_zone();
	
	return zone->calloc(zone, num_items, size);
}

继续借助lldb,追到nano_calloc

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
	size_t total_bytes;
 
	if (calloc_get_size(num_items, size, 0, &total_bytes)) {
		return NULL;
	}
 
	if (total_bytes <= NANO_MAX_SIZE) {
		void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
		if (p) {
			return p;
		} else {
			/* FALLTHROUGH to helper zone */
		}
	}
	malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	return zone->calloc(zone, 1, total_bytes);
}

这里给NANO_MAX_SIZE留个印象,跟进_nano_malloc_check_clear

static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
	MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
 
	void *ptr;
	size_t slot_key;
	size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
	mag_index_t mag_index = nano_mag_index(nanozone);
 
	nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
 
	ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
	......
}

终于,在segregated_size_to_fit终于找到了:

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
	size_t k, slot_bytes;
 
	if (0 == size) {
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
	slot_bytes = k << SHIFT_NANO_QUANTUM;							// multiply by power of two quanta size
	*pKey = k - 1;													// Zero-based!
 
	return slot_bytes;
}

我们来看这两个宏定义:

#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	//16

基本可以得出:内存对齐按照16字节对齐。本次我们传进的size是24,调用calloc(1, 24)时,最后通过算法是通过(24 + 16 - 1) >> 4 << 4 操作 ,结果就是32。

我们再来看一下NANO_MAX_SIZE

#define NANO_MAX_SIZE       256 /* Buckets size {16, 32, 48, 64, 80, 96, 112, ...}*/

它的注释同样也证明了我们的结论,编译器为了更快的分配内存,也有自己的内存对齐原则,即按照16字节对齐。