之前分享过NSObject对象的内存大小,但是在我们日常开发中使用的类一般都是继承于NSObject,那么这些类它们的内存大小又是如何分配的呢?
先来一道开胃菜:
我定义了一个继承于NSObject的Person类如下:
@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对象的地址然后回车:
这个页面显示的是16进制的,一个16进制位代表4个二进制位,那两个16进制位就代表8个二进制位,8位就是一个字节,所以我们从头数,49是第一个字节,82是第二个字节......数到16个,你就会发现我们给
_age和_no的赋值。
或是通过lldb的指令来查看:
虽然这些方式好像都不是很严谨,但也稍微能从侧面证实一下malloc_size(const void *ptr)给出的结果,所以Student的内存应该就是这样的:
开始上正菜:
我们给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函数了。
我们在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字节对齐。