既然是底层原理系列,内存肯定是我们绕不过的一个知识点,今天这篇文章主要是通过源码来探索下OC底层是怎么进行内存对齐的
既然要探索内存相关的东西,那么首先我们要先掌握获取内存的三种方式如下:
-
sizeof()
: 传入的参数为数据类型,获取对应类型所占用的内存.例如:int
long
double
[NSObject alloc]
-
class_getInstanceSize
: 获取实例对象的内存大小,即实际需要的内存,大小随类的属性而变化 -
malloc_size
: 计算实际分配的内存的大小
我们都知道OC对象编译到底层是以结构体的形式存在的,那么我们就优先从结构体的内存对齐开始探索
结构体内存对齐验证
作为准备条件,我们先看下各个类型所占用的内存大小,如下表格:
C | OC | 32位 | 64位 |
---|---|---|---|
bool | BOOL(64位) | 1 | 1 |
signed char | (__signed char)int8_t、BOOL(32位) | 1 | 1 |
unsigned char | Boolean | 1 | 1 |
short | int16_t | 2 | 2 |
unsigned short | unichar | 2 | 2 |
int int32_t | NSInterger(32位)、boolean_t(32位) | 4 | 4 |
unsigned int | boolean_t(64位)、NSUinteger(32位) | 4 | 4 |
long | NSInteger(64位) | 4 | 8 |
unsigned long | NSUInteger(64位) | 4 | 8 |
long long | int64_t | 8 | 8 |
float | CGFloat(32位) | 4 | 4 |
double | CGFloat(64位) | 8 | 8 |
接下来我们自定义两个结构体,看下他们的内存情况,如下:
struct LYStruct1 {
int a;
double b;
char c;
short d;
} struct1;
struct LYStruct2 {
double b;
int a;
short d;
char c;
} struct2;
NSLog(@"%lu--%lu", sizeof(struct1), sizeof(struct2));
struct1:24--struct2:16
通过sizeof
我们可以发现对于元素相同的两个结构体,会由于元素的先后位置不同而导致他们的内存不同.由此我们证明了确实存在内存对齐的现象
既然确实存在内存对齐,那么内存对齐就一定会遵循一定的规则,接下来我们一起来看下内存对齐的规则到底是怎样的
结构体内存对齐规则
- 数据成员对⻬规则:结构
struct
(或联合体union
)的数据成员,第一个数据成员放在offset
为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int
为4字节,则要从4的整数倍地址开始存储。min(当前开始的位置m n) m = 9 n = 4 9 10 11 12
,即要从第12位开始存储该int
结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(
struct a
里存有struct b
,b
里有char, int, double
等元素,那b
应该从8(最大元素位double
,占8位)的整数倍开始存储.)收尾工作:结构体的总大小,也就是
sizeof
的结果,.必须是其内部最大成员的整数倍.不足的要补⻬.例如结构体中最大元素为double
,那么sizeof
得出的结果一定是8的整数倍.
有了以上规则,接下来我们一起验证下这些规则的准确性
结构体对齐规则验证
以最开始我们定义的struct1
和struct2
为例,具体如下:
根据第一条以及第三条规则,我们得出如下内存布局图
解析:
struct1
-
int a
占用4个字节,从0开始布局为:0~3
-
double b
占用8个字节,min(4, 8)
,由于4不是8的整数倍,所以需要从8开始布局为:8~15
-
char c
占用1个字节,min(16, 1)
,16刚好是1的整数倍,所以从16开始布局为:16
-
short d
占用2个字节,min(17, 2)
,17不是2的整数倍,所以从18开始布局为:18~19
- 根据第三条规则,
struct1
中的最大元素为double b
,占8字节,所以结构体总大小需为8的整数倍,自动补齐得到结构体的大小为24
struct2
-
double b
占用8个字节,从0开始布局为:0~7
-
int a
占用4个字节,min(8, 4)
,由于8是4的整数倍,所以从8开始布局为:8~11
-
short d
占用2个字节,min(12, 2)
,12是2的整数倍,所以从12开始布局为:12~13
-
char c
占用1个字节,min(14, 1)
,14刚好是1的整数倍,所以从14开始布局为:14
- 根据第三条规则,
struct2
中的最大元素为double b
,占8字节,所以结构体总大小需为8的整数倍,自动补齐得到结构体的大小为16
至此结合我们的打印结果我们已经证明了规则第一条以及第三条的正确性,接下来我们来看下结构体嵌套结构体的情况,如下:
struct LYStruct1 {
int a;
double b;
char c;
short d;
struct LYStruct2 {
double a;
int b;
short d;
char c;
} struct2;
} struct1;
NSLog(@"struct1:%lu", sizeof(struct1));
struct1:40
- 根据规则二,结构体
str2
需要从其最大成员的整数倍开始存储,最大成员为double b
,占8字节,所以要从24开始存储
以上,关于结构体的内存对齐我们就全部了解了,接下来我们继续探索下OC对象的内存情况
OC对象内存对齐以及内存优化
OC内存对齐
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16
来改变这一系数,其中的n
就是你要指定的“对齐系数”。在iOS
中,Xcode
默认为#pragma pack(8)
,即8字节对齐
但是细心的小伙伴应该也发现了,我们在探索alloc
源码时提到过字节对齐是以16字节进行对齐的,那么到底是8字节还是16字节呢??
对于这个问题,我们需要区分两个概念,一个是对象实际需要的内存,另一个是系统开辟的内存.还记得我们最开始提到的获取内存的三种方式么,现在我们就通过这几种方式来验证一下,代码如下:
@interface LYPerson : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end
LYPerson *person = [[LYPerson alloc] init];
person.name = @"LiuYan";
person.nickName = @"LY";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LYPerson class]),malloc_size((__bridge const void *)(person)));
2020-09-13 17:43:13.212922+0800 结构体内存对齐[4787:185539] <LYPerson: 0x600003e7c7b0> - 8 - 40 - 48
通过上面的代码我们发现,实际开辟的内存根实际需要的内存并不相同,为了探索为什么会这样,我们查看class_getInstanceSize
和malloc_size
源码发现
class_getInstanceSize 源码
/**
* Returns the size of instances of a class.
*
* @param cls A class object.
*
* @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
*/
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
⬇️
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() const {
return word_align(unalignedInstanceSize());
}
⬇️
static inline uint32_t word_align(uint32_t x) {
//x+7 & (~7) --> 8字节对齐
return (x + WORD_MASK) & ~WORD_MASK;
}
//其中 WORD_MASK 为
# define WORD_MASK 7UL
malloc_size核心源码
#define SHIFT_NANO_QUANTUM 4
#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;
}
通过源码我们可以发现class_getInstanceSize
确实采用的的8字节对齐,malloc_size
采用的是16字节对齐.
但是理论上8字节对齐已经够用了的,苹果为什么在分配内存的时候还要采用16字节对齐呢,这一点主要是因为apple系统为了防止一切的容错,因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展
内存优化
通过前面对结构体内存对齐的分析,我们知道内存的大小跟属性的先后顺序是有关系的,因此苹果在底层做了一些关于内存优化的操作来提高性能,即属性重排,大概就是将占用内存大的属性尽可能放在前面.