0%

iOS的内存对齐

上文中在分析alloc函数的时候,发现有个步骤是给对象分配内存空间,在分配内存空间的时候有一个内存对齐的概念,本文就详细说明一下内存对齐。

NSObject对象占用多少内存空间

看下面这段代码

1
2
3
4
NSObject *obj = [[NSObject alloc] init];
NSLog(@"实际占用: class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
NSLog(@"系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
NSLog(@"NSObject类型占用:sizeOf = %zd", sizeof(obj));

可以看到打印结果是

1
2
3
实际占用: class_getInstanceSize = 8
系统分配:malloc_size = 16
NSObject类型占用:sizeOf = 8

说明NSObject对象占用的内存空间是8个字节,但是系统分配的空间是16字节。这是为什么,我们看一下核心代码,根据上文可以知道,各个函数的调用是allocWithZone->_objc_rootAllocWithZone->_class-createInstanceFromZone,前两个函数都是简单的调用后一个函数,代码集中在_class-createInstanceFromZone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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);
}

最后我们看instanceSize函数

1
2
3
4
5
6
7
8
9
10
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;
}

可以看到当size<16的时候,系统会分配16个字节。

内存对齐规则

1、结构体变量的首地址是其最长基本类型成员的整数倍;

2、结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足;

3、结构体的总大小为结构体最大基本类型成员变量大小的整数倍;

4、结构体中的成员变量都是分配在连续的内存空间中。

自定义一个类验证一下

1
2
3
4
5
6
@interface TestMemory : NSObject{
int _age;
NSString *_name;
int _weight;
}
@end

像刚开始一样,我们打印TestMemory对象的内存大小

1
2
3
4
TestMemory *obj = [[TestMemory alloc] init];
NSLog(@"实际占用: class_getInstanceSize = %zd", class_getInstanceSize([TestMemory class]));
NSLog(@"系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
NSLog(@"TestMemory类型占用:sizeOf = %zd", sizeof(obj));

打印结果

1
2
3
实际占用: class_getInstanceSize = 32
系统分配:malloc_size = 32
TestMemory类型占用:sizeOf = 8

TestMemory转成C++代码看一下结构体内容

1
xcrun -sdk iphonesimulator clang -rewrite-objc TestMemory.m

结构体内容

1
2
3
4
5
6
struct TestMemory_IMPL {
struct NSObject_IMPL NSObject_IVARS;//8个字节
int _age;//int 正常4个字节
NSString *_name;//8个字节
int _weight;//4个字节
}

根据内存对齐规则2,_name变量的起始地址将会是8的整数倍,_age变量后面空了4个字节,可以计算出大小为8+4+4(填充4字节)+8+4=28,根据规则3,需要是最大成员的整数倍,所以28向上对8取整,内存大小为32。

下面我们修改一下TestMemory成员变量的顺序

1
2
3
4
5
6
@interface TestMemory : NSObject{
int _age;
int _weight;
NSString *_name;
}
@end

现在打印的结果是:

1
2
3
实际占用: class_getInstanceSize = 24
系统分配:malloc_size = 32
TestMemory类型占用:sizeOf = 8

可以看到实际占用的大小为24了,我们看一下c++的结构体代码

1
2
3
4
5
6
struct TestMemory_IMPL {
struct NSObject_IMPL NSObject_IVARS;//8字节
int _age;//4字节
int _weight;//4字节
NSString *_name;//8字节
};

因为我们调整了一下顺序,_name的起始地址是8的倍数,所以不需要再_weight字节后面填充字节,大小就为8+4+4+8=24,因为24是8的倍数,所以也不需要根据规则3对8向上取整,实际占用内存空间就是24。

到这里我们可以得出一个结论,成员变量顺序不同可以影响对象站内内存的空间大小。现在我们可能会想如果我们实际开发中定义属性的顺序会不会影响内存大小呢,如果影响如果不注意岂不是对象的内存大小会有很大差别?带着这个问题我们看一下定义属性的c++代码。

1
2
3
4
5
6
7
8
9
10
@interface TestMemory : NSObject{
// int _age;
// int _weight;
// NSString *_name;
}
@property (nonatomic,assign)int age;
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)int weight;

@end

我们定义属性的顺序和第一次测试的顺序相同。c++代码

1
2
3
4
5
6
struct TestMemory_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _weight;
NSString * _Nonnull _name;
};

可以看到结构体里成员变量的顺序已经改变过了,说明苹果已经进行了优化,我们实际开发定义属性的时候,不会因为属性定义顺序的不同而影响对象占用的内存大小。

参考文档