iOS 内存对齐

前言

现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐

OC对象的内存占用

我们先来看看OC的对象所占用的内存

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字节。
这个可以从源码
中看一看,我下载是objc4-779
在NSObject.mm中查看

1
2
3
4
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

然后去看_objc_rootAllocWithZone的实现,在objc-runtime-new.mm中

1
2
3
4
5
6
7
8
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}

接着看_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
46
47
48
49
50
51
52
53
/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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)的实现

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

到这里就能看到当实例对象不足16个字节,系统会分配给16个字节
所以我们可以得出结论
在64位架构下, 系统分配了16个字节给NSObject对象(通过malloc_size函数获得);
但NSObject对象内部只使用了8个字节的空间(可以通过class_getInstanceSize函数获得)。

内存对齐

内存对齐的规则

  • 结构体变量的首地址是其最长基本类型成员的整数倍;
  • 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足;
  • 结构体的总大小为结构体最大基本类型成员变量大小的整数倍;
  • 结构体中的成员变量都是分配在连续的内存空间中。

下面我们创建一个OC类来验证一下

1
2
3
4
5
6
7
8
9
10
11
12
@interface MemoryObject : NSObject {
int _age;
NSString *_name;
int _height;
}

//@property (nonatomic, assign) int age;
//@property (nonatomic, copy) NSString *name;
//@property (nonatomic, assign) int height;


@end

打印一下内存

1
2
3
4
MemoryObject *m = [[MemoryObject alloc] init];
NSLog(@"class_getInstanceSize = %zd", class_getInstanceSize([MemoryObject class]));
NSLog(@"malloc_size = %zd", malloc_size((__bridge const void *)(m)));
NSLog(@"sizeOf = %zd", sizeof(m));

输出

1
2
3
class_getInstanceSize = 32
malloc_size = 32
sizeOf = 8

我们把MemoryObject转成C++代码看下

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

在C++代码中我们可以看到MemoryObject的结构

1
2
3
4
5
6
struct MemoryObject_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 指针占用8个字节
int _age; //4
NSString *_name; //8
int _height; //4
};

如果没有内存对齐的话,内存占用应该是8+4+8+4 = 24个字节,现在根据内存对齐的规则(在结构体中,总大小为结构体对最大成员大小的整数倍,如不满足,最后填充字节以满足,可分配的最小内存是结构体中内存占用最大的成员变量的大小。)由于需要满足8的整数倍,所以最后填充字节分配32个字节
image.png
结构体中成员变量的内存都是连续分配,由于_age只有4字节,按照内存对齐规则需要分配8个字节,而_height在最后也会因为内存对齐填充成8字节

而系统实际分配了32个字节,因为之前看源码,系统每次至少分配16个字节,这里的32是16的整数倍。
如果我们调整一下顺序

1
2
3
4
5
@interface MemoryObject : NSObject {
int _age;
int _height;
NSString *_name;
}

对应的C++代码

1
2
3
4
5
6
struct MemoryObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
NSString *_name;
};

再次输出内存大小

1
2
3
class_getInstanceSize = 24
malloc_size = 32
sizeOf = 8

内存分配应该是这样的了
image.png
按照内存对齐规则,_age和_height加起来正好是8字节,不用系统填充,所以内存整个结构体占用24个字节,而系统需要满足16的倍数,还是32个字节。
所以成员变量的顺序是可以影响内存分配的
不过如果我们代码里用property声明的话

1
2
3
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int height;

对应的C++代码是这样的

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

顺序被调整过了,而且实际内存占用也是24个字节,可见使用property的时候苹果是做了这方面的优化的.

参考

关于NSObject对象的内存布局,看我就够了!