OC内存大小的相关计算

在面试的过程中,我们较大概率地会被问一个类所占的内存大小。本篇博客从下面一段测试代码开始分析整个内存大小的计算过程。

测试代码如下:

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
54
55
56
57
58
59
struct A {
} TestA;

struct AA {
char a;
} TestAA;

struct AAA {
char a;
int b;
} TestAAA;

@interface Person: NSObject {
int _a;
}

@end

@implementation Person
@end

@interface Student1: Person {
int _b;
}

@end

@implementation Student1
@end

@interface Student2: Person {
int _b;
int _c;
}

@end

@implementation Student2
@end

// 测试
+ (void)test {
NSLog(@"TestA sizeof: %lu",sizeof(TestA));
NSLog(@"TestAA sizeof: %lu",sizeof(TestAA));
NSLog(@"TestAAA sizeof: %lu",sizeof(TestAAA));
NSLog(@"--------------------------------------");
NSLog(@"NSObject class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
NSLog(@"NSObject malloc_size = %zd", malloc_size((__bridge const void*)[NSObject new]));
NSLog(@"--------------------------------------");
NSLog(@"Person class_getInstanceSize = %zd", class_getInstanceSize([Person class]));
NSLog(@"Person malloc_size = %zd", malloc_size((__bridge const void*)[Person new]));
NSLog(@"--------------------------------------");
NSLog(@"Student1 class_getInstanceSize = %zd", class_getInstanceSize([Student1 class]));
NSLog(@"Student1 malloc_size = %zd", malloc_size((__bridge const void*)[Student1 new]));
NSLog(@"Student1 sizeof = %zd", sizeof([Student1 class]));
NSLog(@"--------------------------------------");
NSLog(@"Student2 class_getInstanceSize = %zd", class_getInstanceSize([Student2 class]));
NSLog(@"Student2 malloc_size = %zd", malloc_size((__bridge const void*)[Student2 new]));
}

执行结果(运行在模拟器下)如下:
class_getInstanceSize&malloc_size

其中class_getInstanceSize指的是成员变量占用的内存大小,malloc_size指的是指针指向内存空间的大小即实际分配的内存大小。

关于内存大小的计算主要依赖于运行环境以及内存对齐。上面的都是运行在arm64环境下,因此内存对齐决定了它们的值为什么不同。

内存对齐

内存对齐说白了就是为了提高CPU寻址操作性能的一种规则。我们可以通过#pragma pack(n),n=1、2、4、8、16 来改变这一系数,其中的n就是要指定的“对齐系数”。内存对齐的规则如下:

  1. 数据成员对齐规则:结构体或联合体的第一个数据成员放在偏移为0的位置,以后每个数据成员的位置为min(对齐系数,自身长度)的整数倍,下个位置不为本数据成员的整数倍位置的自动补齐。
  2. 数据成员为结构体:该数据成员的内最大长度的整数倍的位置开始存储。
  3. 整体对齐规则:数据成员按照1,2步骤对齐之后,其自身也要对齐,对齐原则是min(对齐系数,数据成员最大长度)的整数倍。

内存对齐计算

在64位编译器环境下

代码示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对齐系数为8
#pragma pack(8)
struct AA {
int a; // 4字节
char b; // 1字节
short c; // 2字节
char d; // 1字节
} Test1AA;
#pragma pack()

int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"Test1AA: %lu",sizeof(Test1AA));
}
}

执行结果:

1
Test1AA: 12

计算过程如下:
内存对齐计算1

代码示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma pack(8)
struct AA {
char a[2];
short b;
struct BB {
int a;
double b;
float c;
} Test2BB;
} Test2AA;
#pragma pack()

int main(int argc, char * argv[]) {

@autoreleasepool {
NSLog(@"Test2AA: %lu",sizeof(Test2AA));
}
}

执行结果:

1
Test2AA: 32

计算过程如下:
内存对齐计算2

OC类的内存分析

相关函数

class_getInstanceSize

上面讲到class_getInstanceSize表示成员变量所占的内存大小,那么对于Person类创建的实例来说,它的成员变量大小应该为12,为什么结果却是16。

class_getInstanceSize的内存也有它自己的内存对齐,通过objc源码中的class_getInstanceSize的底层实现可以知道,class_getInstanceSize的实现依赖于底层函数word_align,该函数返回的结果是8的倍数,另外从alloc函数开始进行分析,到instanceSize函数中可以知道所有对象的内存大小至少是16个字节,所以对象申请的内存空间是以8字节进行内存对齐且至少是16个字节。

word_align实现如下:

1
2
3
4
5
6
7
8
9
10
11
static inline uint32_t word_align(uint32_t x) {
// WORD_MASK在64下的定义为7UL,就是7,所以相当于(x + 7) & ~7
// 0000 0111 -> 7
// x:12,12就是Person类中成员变量的大小
// 12+7 = 19
// 0001 0011 -> 19
// &
// 1111 1000 -> ~7 ~运算,二进制中,0变1,1变0.
// 0001 0000 -> 16
return (x + WORD_MASK) & ~WORD_MASK;
}

malloc_size

通过malloc源码中的segregated_size_to_fit函数可以知道系统开辟内存空间是以16字节进行内存对齐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {
// NANO_REGIME_QUANTA_SIZE: (1 << SHIFT_NANO_QUANTUM) 即 16
size = NANO_REGIME_QUANTA_SIZE;
}

// size: 8
// 0000 1000 -> 8
// size + NANO_REGIME_QUANTA_SIZE - 1 = 8 + 15 = 23
// 0001 0111 -> 23
// >> 4
// 0000 0001
// << 4
// 0001 0000 -> 16
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;
*pKey = k - 1;

return slot_bytes;
}

Person、Student的内存分析

回到一开始的测试代码,根据上面内存对齐的3个规则,Person、Student1、Student2的实际分配内存(malloc_size)计算过程如下:
内存对齐计算3

使用View Memory查看内存

xcode_view_memory

从上面这张图中我们可以知道,两个16进制代表1个字节,一行有32个字节。01 00 00 0003 00 00 0005 00 00 00(小端)对应的就是_a_b_c,另外前8个字节就是isa。整体内存布局也与我们手动计算实际内存分配的结果一致。

sizeof

sizeof用来返回类型的大小,其内部也是进行了内存对齐的。将测试代码中的结构体AAAAAA通过内存对齐规则进行分析,确实得到018

这里我们使用结构体AA进行举例:

  • 根据规则1,数据成员占1个字节,位于0号地址;
  • 无结构体成员变量,跳过规则2;
  • 根据规则3,min(1, n) = 1,取1的整数倍,即结构体分配的内存大小为1个字节。

关于使用sizeof去获取OC类内存大小的时候,我发现一个比较有意思的东西。

测试代码如下:

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
54
55
56
57
58
struct N_NSObject_IMPL {
Class isa;
};

struct N_Person_IMPL {
struct N_NSObject_IMPL NSObject_IVARS;
int _a;
};

struct N_Student_IMPL {
struct N_Person_IMPL Person_IVARS;
int _b;
char _c;
};

@interface Person : NSObject {
int _a;
}

@end

@implementation Person
@end

@interface Student : Person {
int _b;
char _c;
}

@end

@implementation Student
@end

// 测试
+ (void)test {
NSObject *o = [NSObject new];
struct N_NSObject_IMPL *so = (__bridge struct N_NSObject_IMPL *)o;
NSLog(@"[NSObject new] sizeof = %lu", sizeof(o));
NSLog(@"[NSObject class] sizeof = %lu", sizeof([NSObject class]));
NSLog(@"struct N_NSObject_IMPL sizeof = %lu", sizeof(struct N_NSObject_IMPL));
NSLog(@"*so sizeof = %lu", sizeof(so));
NSLog(@"--------------------------------------");
Person *p = [Person new];
struct N_Person_IMPL *sp = (__bridge struct N_Person_IMPL *)p;
NSLog(@"[Person new] sizeof = %lu", sizeof(p));
NSLog(@"[Person class] sizeof = %lu", sizeof([Person class]));
NSLog(@"struct N_Person_IMPL sizeof = %lu", sizeof(struct N_Person_IMPL));
NSLog(@"*sp sizeof = %lu", sizeof(sp));
NSLog(@"--------------------------------------");
Student *s = [Student new];
struct N_Student_IMPL *ss = (__bridge struct N_Student_IMPL *)s;
NSLog(@"[Student new] sizeof = %lu", sizeof(s));
NSLog(@"[Student class] sizeof = %lu", sizeof([Student class]));
NSLog(@"N_Student_IMPL sizeof = %lu", sizeof(struct N_Student_IMPL));
NSLog(@"Student malloc_size = %zd", malloc_size((__bridge const void*)s));
NSLog(@"*ss sizeof = %lu", sizeof(ss));
}

执行结果:

打印sizeof

N_NSObject_IMPLN_Person_IMPLN_Student_IMPL是将OC转成C++代码时对应的结构,使用sizeof获取这些结构体大小的时候,值与class_getInstanceSize的值是一样的。

使用sizeof获取指针、实例、类其结果都是8,这又是为什么呢?个人认为传实例和类的时候可以看做传的其实就是对应的指针。指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,地址基本就是整形,因此无论用什么类、对象作为sizeof的参数(这里就将sizeof看成是一个函数),其结果都一样的。

最后再总结下class_getInstanceSizemalloc_sizesizeof的区别:

  • class_getInstanceSize表示成员变量的所占的内存大小
  • malloc_size表示实际分配的内存大小
  • sizeof表示变量或者类型的大小,传入结构体,返回的则是结构的大小,传入指针(这里的指针表示C指针,OC的引用)即传入值,则返回传入值的类型大小