之前分享过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字节对齐。