0%

iOS底层探索 - 内存对齐

在上一篇alloc方法的探索中,我们提到了内存对齐的概念,这篇文章我们就主要介绍一下内存对齐

结构体的字节对齐

一个例子

我们首先定义几个结构体,初始化,看一下每个结构体的内存大小。

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
struct JSPerson1 {
double a;
char b;
int c;
short d;
}struct1;
struct JSPerson2 {
double a;
int b;
char c;
short d;
}struct2;
struct JSPerson3 {
double a;
int b;
char c;
short d;
int e;
struct JSPerson1 str;
int f;
}struct3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"struct1的大小==%lu",sizeof(struct1));
NSLog(@"struct2的大小==%lu",sizeof(struct2));
NSLog(@"struct3的大小==%lu",sizeof(struct3));
}
return 0;
}

可以看到打印结果为:

1
2
3
struct1的大小==24
struct2的大小==16
struct3的大小==56

JSPerson1JSPerson2的属性数量相同,为什么打印的大小结果不一样呢,这里就引入了字节对齐的概念,我们先看一下OC中各类型占用的内存大小:

类型占用空间

结构体对齐原则

  • 1、数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。
  • 2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  • 3、收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员(基本数据类型)的整数倍.不足的要补⻬。

根据上面的表格和规则我们分析一下例子中三个结构体占用的内存大小(64位系统)

  • 首先看struct1

    1
    2
    3
    4
    5
    6
    struct JSPerson1 {
    double a;
    char b;
    int c;
    short d;
    }struct1;

    根据规则1,a变量存储的空间是[0,7],b变量存储的位置是[8],c因为本身是int4个字节大小,所以不能从9的位置开始存储,要从4整数倍开始存储所以c存储的空间是[12,15],dshort类型大小为2162的整数倍,所以d的存储位置是[16,17]。根据规则3,结构体的总大小需要是内部最大成员的整数倍,所以struct1的大小为24.

  • 再看struct2

    1
    2
    3
    4
    5
    6
    struct JSPerson2 {
    double a;
    int b;
    char c;
    short d;
    }struct2;

    和上面一样a变量存储的空间是[0,7]b变量存储的位置是[8,11]c变量的存储空间是[12],d变量的存储空间是[14,15]

    根据规则3struct2的大小为16

  • 最后看struct3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct JSPerson3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct JSPerson1 str;
    int f;
    }struct3;

    前四个变量和struct2类似,a变量存储的空间是[0,7]b变量存储的位置是[8,11]c变量的存储空间是[12],d变量的存储空间是[14,15],e变量存储空间是[16,19],结构体成员比较特殊我们根据规则2,str成员的起始地址要是它内部最大成员的大小的整数倍也就是8,所以str存储的空间是[24,47],f所占的内存空间是[48,51],注意这个地方有个点:结构体的总大小是其最大基本成员变量的大小的整数倍,所以struct3的内存大小是56(8的整数倍)。

对象的字节对齐

看完了结构体的内存对齐,我们看一下对象的,其实我们很容易可以想到,对象其实也是一个结构体,它和结构体的字节对齐规则也是一样的。我们来验证一下。

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
@interface Person1 : NSObject{
int _age;
int _weight;
NSString *_name;
}
@end
@implementation Person1

@end

@interface Person2 : NSObject{
int _age;
NSString *_name;
int _weight;
}
@end

@implementation Person2

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
Person1 *obj1 = [[Person1 alloc] init];
NSLog(@"Person1实际占用: class_getInstanceSize = %zd", class_getInstanceSize([Person1 class]));
NSLog(@"Person1系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj1)));
Person2 *obj2 = [[Person2 alloc] init];
NSLog(@"Person2实际占用: class_getInstanceSize = %zd", class_getInstanceSize([Person2 class]));
NSLog(@"Person2系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj2)));
}
return 0;
}

我们定义两个类Person1Person2,分别打印他们实际占用内存和系统分配内存大小:

1
2
3
4
Person1实际占用: class_getInstanceSize = 24
Person1系统分配:malloc_size = 32
Person2实际占用: class_getInstanceSize = 32
Person2系统分配:malloc_size = 32

这里有个注意的点就是不要漏掉isa指针,对象结构体中第一个成员变量就是isa,所以我们分析的方法和结构体一样,Person1isa存储地址是[0,7],_age的存储空间是[8,11],_weight的存储空间是[12,15]_name的存储空间是[16,23],所以Person1实际空间是24,同样的方法可得Person2的实际空间是32

这里我们可以看到,成员变量的顺序是会影响到对象实际占用的内存大小,类的属性最后其实也是会生成对应的成员变量,那么属性的定义顺序是不是也会影响对象的实际占用内存大小呢,我们带着这个问题继续探索:

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
@interface Person1 : NSObject

@property (nonatomic,assign)int age;
@property (nonatomic,assign)int weight;
@property (nonatomic,strong)NSString *name;

@end

@implementation Person1

@end

@interface Person2 : NSObject

@property (nonatomic,assign)int age;
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)int weight;

@end

@implementation Person2

@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person1 *obj1 = [[Person1 alloc] init];
NSLog(@"Person1实际占用: class_getInstanceSize = %zd", class_getInstanceSize([Person1 class]));
NSLog(@"Person1系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj1)));
Person2 *obj2 = [[Person2 alloc] init];
NSLog(@"Person2实际占用: class_getInstanceSize = %zd", class_getInstanceSize([Person2 class]));
NSLog(@"Person2系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj2)));
}
return 0;
}

我们把成员变量换成属性继续打印上面的内容:

1
2
3
4
Person1实际占用: class_getInstanceSize = 24
Person1系统分配:malloc_size = 32
Person2实际占用: class_getInstanceSize = 24
Person2系统分配:malloc_size = 32

发现这次打印Person1Person2实际占用的内存大小是相同的,这是为什么呢?我们将main.m文件转换成c++代码看一下:

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

我们打开main.cpp文件,搜索Person1Person2:

1
2
3
4
5
6
7
8
9
10
11
12
struct Person1_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _weight;
NSString * _Nonnull _name;
};
struct Person2_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _weight;
NSString *_name;
};

看到这里我们明白了,原来编译器帮我们做了一层优化,将属性按照最优的顺序转换成成员变量。所以我们定义属性的顺序并不会影响到对象实际占用的内存大小。

内存对齐的内容就探索到这里,后面会继续探索ios 底层