前话:
在了解内存对齐之前先了解一下各数据类型在内存中的大小,目前我们比较常用的是64位系统,所以我们的研究对象统一采用64位的大小作为参考。
一. 如何获取内存的大小
获取NSObject对象的内存大小,需要用到以下几个函数:
- 1.class_getInstanceSize
- 2.malloc_size
- 3.sizeOf
我们先来一段代码,然后调用上面的几函数,看一下结果
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
NSLog(@"malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
NSLog(@"sizeOf = %zd", sizeof(obj));
}
return 0;
}
控制台打印如下:
class_getInstanceSize = 8
malloc_size = 16
sizeOf = 8
结果是 大小都不一样,为什么呢?我们带着疑问来对上面几个函数进行分析一下吧。
1.1 class_getInstanceSize
我们先通过源码来看一下class_getInstanceSize,实际调用的是alignedInstanceSize
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
从以上看出unalignedInstanceSize中指出返回的是类的属性的大小总和,然后进行字节对齐word_align, 其中WORD_MASK为7UL,即按8位补齐。
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
static inline uint32_t word_align(uint32_t x) {
//以下以 uint32_t x = 8 举例,其中 WORD_MASK = 7UL
//第一种算法 ,15 & ~7
// 7+8 = 15
// 0000 1111 // 代表15
// 0000 1000 // ~7
//& //与操作
// 0000 1000 8 //结果是8
//第二种算法 ,位移3位
// 0000 1111 // 代表15
// 0000 1111 >> 3 // 为0000 0001
// 0000 0001 << 3 // 为0000 1000 //即 8
// (x + 7) >> 3 << 3
return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
小 结:class_getInstanceSize依赖于<objc/runtime.h>,返回创建一个实例对象所需内存大小。就是获取对象的全部属性的大小总和,然后按8位对齐获得,不足8位补齐8位。
?疑问点:目前例子中的对象实例化,但是没有属性,但是为什么还是占位8字节。
答:该8字节是对象的实例化后isa的大小8字节。(后续再详细探讨)
1.2.malloc_size
这个函数主要获取系统实际分配的内存大小,具体的底层实现也可以在源码libmalloc找到,依赖于<objc/runtime.h>,具体如下:
extern size_t malloc_size(const void *ptr);
size_t
malloc_size(const void *ptr)
{
size_t size = 0;
if (!ptr) {
return size;
}
(void)find_registered_zone(ptr, &size);
return size;
}
核心的方法是find_registered_zone,具体如下:
static inline malloc_zone_t *find_registered_zone(const void *, size_t *) __attribute__((always_inline));
static inline malloc_zone_t *
find_registered_zone(const void *ptr, size_t *returned_size)
{
// Returns a zone which contains ptr, else NULL
if (0 == malloc_num_zones) {
if (returned_size) {
*returned_size = 0;
}
return NULL;
}
// first look in the lite zone
if (lite_zone) {
malloc_zone_t *zone = lite_zone;
size_t size = zone->size(zone, ptr);
if (size) { // Claimed by this zone?
if (returned_size) {
*returned_size = size;
}
// Return the virtual default zone instead of the lite zone - see <rdar://problem/24994311>
return default_zone;
}
}
// The default zone is registered in malloc_zones[0]. There's no danger that it will ever be unregistered.
// So don't advance the FRZ counter yet.
malloc_zone_t *zone = malloc_zones[0];
size_t size = zone->size(zone, ptr);
if (size) { // Claimed by this zone?
if (returned_size) {
*returned_size = size;
}
// Asan and others replace the zone at position 0 with their own zone.
// In that case just return that zone as they need this information.
// Otherwise return the virtual default zone, not the actual zone in position 0.
if (!has_default_zone0()) {
return zone;
} else {
return default_zone;
}
}
int32_t volatile *pFRZCounter = pFRZCounterLive; // Capture pointer to the counter of the moment
OSAtomicIncrement32Barrier(pFRZCounter); // Advance this counter -- our thread is in FRZ
unsigned index;
int32_t limit = *(int32_t volatile *)&malloc_num_zones;
malloc_zone_t **zones = &malloc_zones[1];
// From this point on, FRZ is accessing the malloc_zones[] array without locking
// in order to avoid contention on common operations (such as non-default-zone free()).
// In order to ensure that this is actually safe to do, register/unregister take care
// to:
//
// 1. Register ensures that newly inserted pointers in malloc_zones[] are visible
// when malloc_num_zones is incremented. At the moment, we're relying on that store
// ordering to work without taking additional steps here to ensure load memory
// ordering.
//
// 2. Unregister waits for all readers in FRZ to complete their iteration before it
// returns from the unregister call (during which, even unregistered zone pointers
// are still valid). It also ensures that all the pointers in the zones array are
// valid until it returns, so that a stale value in limit is not dangerous.
for (index = 1; index < limit; ++index, ++zones) {
zone = *zones;
size = zone->size(zone, ptr);
if (size) { // Claimed by this zone?
goto out;
}
}
// Unclaimed by any zone.
zone = NULL;
size = 0;
out:
if (returned_size) {
*returned_size = size;
}
OSAtomicDecrement32Barrier(pFRZCounter); // our thread is leaving FRZ
return zone;
}
由于该方法涉及到虚拟内存分配的流程,过于复杂,本文就再详细展开了。理解一点即可,这个函数是获取 系统实际 分配的内存大小,最小16字节。具体可参考上一节的alloc流程。
// alloc创建对象时最小返回16字节
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
总结:malloc_size依赖于<malloc/malloc.h>,返回系统分配给对象的内存大小,而且最小是16字节。下面的第三节会对calloc进行详细分析为什么对象以16字节对齐。
1.3.sizeOf
sizeof是操作符,不是函数,它的作用对象是数据类型,主要作用于编译时。因此,它作用于变量时,也是对其类型进行操作。得到的结果是该数据类型占用空间大小,即size_t类型。
struct test
{
int a; //4 bit
char b; //1 bit
}t1;
NSLog(@"sizeof = %lu ",sizeof(t1));
- 在64位架构下,sizeof(int)得到的是4个字节;
- sizeof(t1),得到的是8个字节
问:int 是4字节,char 是1字节,那么sizeof(t1)的内存不是5字节吗?
答:这里需要考虑内存对齐的问题。关于内存对齐的问题会在后面讲解。
那么对象的sizeof呢?
NSLog(@"sizeof = %zd", sizeof([NSObject class]));
结果是 sizeof = 8
问:为什么是 sizeof= 8 ?
答:因为在64位架构下,自定义一个NSObject对象,无论该对象生命多少个成员变量,最后得到的内存大小都是8个字节。
总结:sizeof 只会计算类型所占用的内存大小,不会关心具体的对象的内存布局。NSObject对象最后只返回内存大小为8字节。
二.内存对齐
在上节中我们发现sizeof(test)的结果是8字节,而不是int 是4字节+char 是1字节 = 5字节。这是因为系统进行了内存对齐的优化处理,接下来我们带着这个疑问了解一下什么是内存对齐。
1.内存对齐是什么?
在iOS开发过程中,编译器会自动的进行字节对齐的处理,并且在64位架构下,是以8字节进行内存对齐的(另32位是以4字节对齐)。
2.内存对齐的原则
对象的属性要内存对齐,而对象本身也需要进行内存对齐
- 1.数据成员对齐原则: 结构(struct)(或联合(union))的数据成员,第
一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小 - 2.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储
- 3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大
成员的整数倍,不足的要补⻬
3.举例说明
说了这么多,看不懂,直接看例子吧!
- 例3.1:结构体 的内存对齐大小计算
struct struct1 {
char a;
double b;
int c;
short d;
} str1;
struct struct2 {
double b;
char a;
int c;
short d;
} str2;
struct struct3 {
double b;
int c;
char a;
short d;
} str3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu——%lu——%lu", sizeof(str1), sizeof(str2), sizeof(str3));
}
return 0;
}
结果:24——24——16
已知(64位)double为8字节,int为4字节,short为2字节,char为1字节
内存对齐原则其实可以简单理解为min(m,n)——m为当前开始的位置,n为所占位数。当m是n的整数倍时,条件满足;否则m位空余,m+1,继续min算法。
以str1为例
1.先求出所有成员变量的偏移量大小,算出总和
- a,长度为1,首地址所以它在第0位坐下了,占据1个格子。[目前长度为1]
- b,长度为8,一开始为min(1,8),1不是8的整数倍,不满足条件直至min(8,8),所以它在第8位坐下了,占据8个格子(即8-15)。[目前长度为16]。
- c ,长度为4,一开始为min(16,4),16是4的倍数,所以它在第16位坐下了,占据4格子(即16-19)。目前长度为20。
- d,长度为2,一开始为min(20,2),满足条件,在20-21位占据。[目前长度22].
2.根据结构体的总大小为结构体最大基本类型成员变量大小的整数倍原则,得到str1中最大的变量大小是b的double(8),此时22不是8的倍数,所以补齐到24.
最终str1的大小为24。
同理str1为24,str3为16。
总结:在结构体中,声明成员变量的顺序不一致,也会导致最终分配内存大小的不同。
- 例3.2:对象 的内存对齐计算
- 例3.2.1 对象单一属性
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface MyAnimal : NSObject{
int _age;
}
@end
@implementation MyAnimal
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyAnimal *myAnimal = [[MyAnimal alloc]init];
myAnimal.age = 1;
NSLog(@"class_getInstanceSize = %zu",class_getInstanceSize([myAnimal class]));
NSLog(@"malloc_size = %lu",malloc_size(CFBridgingRetain(myAnimal)));
NSLog(@"sizeof = %lu",sizeof(myAnimal));
}
return 0;
}
结果:
class_getInstanceSize = 16
malloc_size = 16
sizeof = 8
那它的内存是如何计算的呢?那就看一下,OC代码转换成C++语言,是如何构造数据的。
在终端执行下面的命令,可以将Objective-C对象转换成C/C++语言:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
在main.m文件所在的目录下,继续执行上述讲解的Clang的命令。
在main.cpp文件中,我们搜索查找到Animal类的定义,究其精华如下:
struct NSObject_IMPL {
Class isa;//8字节
};
extern "C" unsigned long OBJC_IVAR_$_MyAnimal$_age;
struct MyAnimal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age; //4字节
};
由上可见,MyAnimal对象最终转为结构体MyAnimal_IMPL,所以MyAnimal_IMPL的大小就是MyAnimal的大小。那我们就可参考上面的结构体来计算对象大小,就是类的isa(8字节)+属性大小的和 = 对象的实际大小。但还得参考内存对齐原则(注:arm64是8字节对齐)。
问:那么class_getInstanceSize 应该是isa(8) + int (4) = 12,为什么是16呢?
答:从上面的1.1可知class_getInstanceSize获取的是对象的实际属性总大小,原来大小确实是12,但arm64位的内存对齐原则,对class_getInstanceSize进行了8位补齐。12不是8的倍数,只有补齐到16位。
具体内存分配如下图所示:
- 例3.2.2 对象多属性
我们再进一步探讨,给MyAnnimal多加2个属性,再看一下输入结果。
@interface MyAnimal : NSObject@interface MyAnimal : NSObject{
int _age;
int _weight;
int _height;
}
@end
@implementation MyAnimal
@end
结果:
class_getInstanceSize = 24
malloc_size = 32
sizeof = 8
问:咦?为什么class_getInstanceSize 和 malloc_size 不相等?
答:我们继续执行一下clang,得到实际大小 8+4+4+4 =20,因内存对齐,所以class_getInstanceSize = 24;
但malloc_size是系统分配的大小,以16位对齐,24不是16的倍数,32才是16的倍数,所以malloc_size = 32。
struct MyAnimal_IMPL {
struct NSObject_IMPL NSObject_IVARS; //8
int _age; // 4
int _weight;// 4
int _height;// 4
};
- 例3.2.3 对象属性顺序的影响1_大括号中声明属性
//第一种情况,不同类型随意放
@interface MyAnimal : NSObject{
//isa ; // 8
int _age;//4
NSString *_name; //8
int _height;//4
NSString *_nick;//8
int _weight;//4
}
//第一种情况
class_getInstanceSize = 48
malloc_size = 48
sizeof = 8
再看一种情况,把相同类型的属性放一起
//第二种情况,把相同类型放一起
@interface MyAnimal : NSObject{
//isa ; // 8
NSString *_name;// 8
NSString *_nick;// 8
int _age;//4
int _height;//4
int _weight;//4
}
//第二种情况
class_getInstanceSize = 40
malloc_size = 48
sizeof = 8
问:为什么都是同一个对象,同样的属性,最后计算出来的实际大小却是不同?
答:还是和内存对齐有关。
- 第1种情况:isa(8),后面的
age(4),紧跟着name(8),age要补齐到8位成为age(8),同理weight(4)也是补齐到weight(8),则全部大小=isa(8) + name(8) + age(8) + nick(8) + weight(8) + height(4) = 44,因对齐8位对齐,则补充到48.
第2种情况:isa(8),后面紧跟着name(8),nick(8),weight(4)和age(4)共用8字节,则全部大小=isa(8) + name(8) + nick(8) + age(4)+ height(4) + weight(4) = 40,因符合8位对齐,则结果是40.
总结:
1.对象的class_getInstanceSize实际大小为isa(8)+属性的大小总和,其中各属性进行8位对齐;
2.对象的系统分配大小malloc_size根据以上的实际大小进行16位对齐;
3.属性的顺序会影响实际内存的大小,相同类型的最好放在一起.
- 例3.2.4 对象属性顺序的影响2_直接用@property声明属性
//第一种情况,不同类型随意放
@interface MyAnimal : NSObject
//isa ; // 8
@property(nonatomic,assign)int age;
@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)int weight;
@property(nonatomic,copy)NSString *nick;
@property(nonatomic,assign)int height;
@end
//第一种情况
class_getInstanceSize = 40
malloc_size = 48
sizeof = 8
再看一种情况,把相同类型的属性放一起
//第二种情况,把相同类型放一起
@interface MyAnimal : NSObject
//isa ; // 8
@property(nonatomic,assign)int age;
@property(nonatomic,assign)int height;
@property(nonatomic,assign)int weight;
@property(nonatomic,copy)NSString *name;
@property(nonatomic,copy)NSString *nick;
@end
//第二种情况
class_getInstanceSize = 40
malloc_size = 48
sizeof = 8
问:真奇怪?直接用@property声明属性,属性的位置不同,但这两种情况的结果居然class_getInstanceSize都是40。为什么呢?
答:我们继续clang看一下cpp代码,两种情况的代码都如下,系统对其属性的位置进行了优化,把相同类型的放在一起,目的是为了节省内存。
struct MyAnimal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _weight;
int _height;
NSString *_name;
NSString *_nick;
};
总结:直接用@property声明属性,顺序不会对内存的大小产生影响,系统会自动对其进行 优化,把相同类型的放在一起。
三.calloc流程分析
在上一章中我们讲到了obj = (id)calloc(1, size),我们当时没有详细进入研究。但是objc源码我们无从下手,现在我们可以通过libmalloc源码来一探究竟。
3.1.calloc
在libmalloc源码中新建target,按照objc源码中的方式调用,我们把之前算出来的对象的属性大小总和40传进来,然后看一下calloc(1, 40)结果。
#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
void *p = calloc(1, 40);
NSLog(@"malloc_size = %lu",malloc_size(p));
}
return 0;
}
结果:malloc_size = 48
问:为什么我们传进来的是40,经过clloc之后,系统分配的大小却是48?
带着这个疑问,我们继续往下看calloc里面的具体实现,调用malloc_zone_calloc
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;
}
3.2. malloc_zone_calloc
我们再往下看关键代码ptr = zone->calloc(zone, num_items, size);
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (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);
return ptr;
}
但是我们进入zone->calloc后发现都是看不懂的东西,zone是malloc_zone_t类型,其中有个方法(calloc))(struct _malloc_zone_t zone, size_t num_items, size_t size);,没法往下进入了。那我们再想一下其他方法。
typedef struct _malloc_zone_t {
/* Only zone implementors should depend on the layout of this structure;
Regular callers should use the access functions below */
void *reserved1; /* RESERVED FOR CFAllocator DO NOT USE */
void *reserved2; /* RESERVED FOR CFAllocator DO NOT USE */
size_t (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
void *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);
void *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
void *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
void (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);
void *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
void (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone); /* zone is destroyed and all memory reclaimed */
const char *zone_name;
/* Optional batch callbacks; these may be NULL */
unsigned (* MALLOC_ZONE_FN_PTR(batch_malloc))(struct _malloc_zone_t *zone, size_t size, void **results, unsigned num_requested); /* given a size, returns pointers capable of holding that size; returns the number of pointers allocated (maybe 0 or less than num_requested) */
void (* MALLOC_ZONE_FN_PTR(batch_free))(struct _malloc_zone_t *zone, void **to_be_freed, unsigned num_to_be_freed); /* frees all the pointers in to_be_freed; note that to_be_freed may be overwritten during the process */
struct malloc_introspection_t * MALLOC_INTROSPECT_TBL_PTR(introspect);
unsigned version;
/* aligned memory allocation. The callback may be NULL. Present in version >= 5. */
void *(* MALLOC_ZONE_FN_PTR(memalign))(struct _malloc_zone_t *zone, size_t alignment, size_t size);
/* free a pointer known to be in zone and known to have the given size. The callback may be NULL. Present in version >= 6.*/
void (* MALLOC_ZONE_FN_PTR(free_definite_size))(struct _malloc_zone_t *zone, void *ptr, size_t size);
/* Empty out caches in the face of memory pressure. The callback may be NULL. Present in version >= 8. */
size_t (* MALLOC_ZONE_FN_PTR(pressure_relief))(struct _malloc_zone_t *zone, size_t goal);
/*
* Checks whether an address might belong to the zone. May be NULL. Present in version >= 10.
* False positives are allowed (e.g. the pointer was freed, or it's in zone space that has
* not yet been allocated. False negatives are not allowed.
*/
boolean_t (* MALLOC_ZONE_FN_PTR(claimed_address))(struct _malloc_zone_t *zone, void *ptr);
} malloc_zone_t;
打断点试一下,我们看到其实调用的是default_zone_calloc,是不是有一种山穷水复疑无路,柳暗花明又一村的感觉。
3.3. 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);
}
按上面的思路,我们给zone = runtime_default_zone()打断点,看一下效果。发现是调用 naco_calloc
3.4. naco_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) { // NANO_MAX_SIZE 256
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 发现为 256 ,此时大小小于256,所以从上面断点往下执行,走到*void p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
3.5. _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));
if (ptr) {
/**因为不走这里,此处省略三千字*/
} else {
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
我们跟着断点往下走,代码执行到了segregated_size_to_fit(nanozone, size, &slot_key)
3.6. segregated_size_to_fit
这个代码是不是有一种似曾相识的感觉,先右移几位,再左移几位。没错,就是我们前面讲的对象的属性8位对齐。此处的NANO_REGIME_QUANTA_SIZE为16,此处我们可以理解为16位对齐。
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
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;
}
再看一下我们原来传进来的size 是 40,在经过 (40 + 16 - 1) >> 4 << 4 操作后,结果为48,也就是16的整数倍——即16字节对齐。
那么我们3.1中提出的问题为什么传进来的是40,系统分配结果却是48的答案就找到了,因为16字节对齐。
总结:calloc进行对象系统分配内存时使用的是16字节对齐,为的是防止内存溢出。
3.7 calloc流程图
参考链接:
关于NSObject对象的内存布局
iOS探索内存对齐&malloc源码
iOS基础知识之@property 和 Ivar 的区别