之前分享过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目标的地址然后回车:

OC对象的本质之:OC的内存对齐
这个页面显示的是16进制的,一个16进制位代表4个二进制位,那两个16进制位就代表8个二进制位,8位便是一个字节,所以咱们从头数,49是第一个字节,82是第二个字节……数到16个,你就会发现咱们给_age_no的赋值。

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

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

虽然这些方法好像都不是很谨慎,但也略微能从侧面证明一下malloc_size(const void *ptr)给出的成果,所以Student的内存应该便是这样的:

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

开始上正菜:

咱们给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函数了。

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

咱们在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字节对齐。