在上一篇文章中 iOS底层源码探究之alloc、init 中我们已经探究了
OC
对象的初始化过程,但是一个对象的大小又跟哪些因素有关呢?或者说一个对象需要开辟的内存空间由哪些因素决定?这里我们就来探究一下。
在这之前,我们先来看一下获取内存大小的三种方式:
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"obj对象类型占用内存的大小:%lu",sizeof(obj));
NSLog(@"obj对象实际占用的内存的大小:%lu",class_getInstanceSize([obj class]));
NSLog(@"obj对象实际分配型内存的大小:%lu",malloc_size((__bridge const void*)(obj)));
}
return 0;
}
2021-08-06 17:31:07.752213+0800 TEXT[32445:473578] obj对象类型占用内存的大小:8
2021-08-06 17:31:07.753828+0800 TEXT[32445:473578] obj对象实际占用的内存的大小:8
2021-08-06 17:31:11.691566+0800 TEXT[32445:473578] obj对象实际分配型内存的大小:16
小结:
sizeof
:计算类型占用的内存大小
,其中可以放 基本数据类型、对象、指针。
对于obj实例对象
而言,其本质就是一个结构体(即 struct objc_object)的指针
,所以sizeof(obj)
打印的是对象obj的指针大小
,我们知道一个指针的内存大小是8
,所以打印是 8
class_getInstanceSize: 计算对象实际占用的内存大小
,这个需要依据类的属性,成员变量而变化
,如果没有自定义属性和成员变量,仅仅只是继承自NSObject,则类的实例对象实际占用的内存大小是8,这是因为NSObject自带一个成员变量isa,是一个结构体指针,占8个字节。
malloc_size: 计算对象实际分配的内存大小
,这个是由系统完成的,从上面的打印结果看出,实际分配的和实际占用的内存大小并不相等,这是因为系统分配内存按16字节对齐,这个问题后面会具体说明。
我们都知道OC经过编译之后会变成C代码,对象会变成C语言中的结构体。接下来我们来研究结构体内存对齐
话不多说,上代码
//定义两个结构体
struct SZstruct1{
char a; //1字节
double b; //8字节
int c; //4字节
short d; //2字节
}SZstruct1;
struct SZstruct2{
double b; //8字节
int c; //4字节
short d; //2字节
char a; //1字节
}SZstruct2;
//结构体嵌套结构体
struct SZstruct3{
char a; //1字节
short d; //2字节
int c; //4字节
double b; //8字节
struct SZstruct2 str2;
}SZstruct3;
//计算 结构体占用的内存大小
NSLog(@"%lu-%lu-%lu",sizeof(SZstruct1),sizeof(SZstruct2),sizeof(SZstruct3));
2021-08-06 17:52:16.593596+0800 TEXT[33297:490443] 24-16-32
问题:两个结构体唯一的区别
只是在于定义变量的顺序不一致
,那为什么他们做占用的内存大小不相等
呢?其实这就是iOS中的内存字节对齐
现象
分析:
内存对齐规则:每个平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n)
,n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”,
在ios中,Xcode默认为#pragma pack(8),即8字节对齐。
内存对齐原则:
原则一、数据成员的对齐规则可以理解为
min(m, n)
的公式, 其中m
表示当前成员的开始位置
,n
表示当前成员所需要的位数
。如果满足条件m 整除 n
(即m % n == 0
),n
从m
位置开始存储, 反之继续检查 m+1 能否整除 n
, 直到可以整除, 从而就确定了当前成员的开始位置。原则二、数据成员为结构体:当结构体嵌套了结构体时,作为数据成员的结构体的
自身长度
作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度
为8原则三、最后
结构体的内存大小
必须是结构体中最大成员内存大小
的整数倍,不足的需要补齐。
验证对齐原则 :
首先看看不同数据类型占内存大小图
SZstruct1内存大小计算详细过程
1、char a: 从第0位开始,占1个字节,根据上面对齐原则一的公式min(0,1),0 % 1 = 0,可以整除,即a存储在第0位;
2、double b:从第1位开始,占8个字节,此时min(1,8),1 % 8 = 1,不能整除,所以位数向后移动直到第8位min(8,8),8 % 8 = 0,可以整除,所以8~15存储b;
3、int c:从第16位开始,占4个字节,此时min(16,4),16 % 4 = 0,可以整除,所以16~19存储C;
4、short d:从20位开始,占2个字节,此时min(20,2),20 % 2 = 0.可以整除,所以20~21存储d
5、根据内存对齐原则三:
最后结构体的内存大小
必须是结构体中最大成员即double b(8)内存大小
的整数倍,不足的需要补齐,所以sizeof(SZstruct1)的结果是24
SZstruct2内存大小计算详细过程
1、double b
: 从第0位开始,占8个字节,根据上面对齐原则一的公式min(0,8),0 % 8 = 0,可以整除,即0~7存储b;
2、int c:从第8位开始,占4个字节,此时min(8,4),8 % 4 = 0,可以整除,所以8~11存储C;
3、short d:从第12位开始,占2个字节,此时min(12,2),12 % 2 = 0,可以整除,所以12~13存储d;
4、
char a:从14位开始,占1个字节,此时min(14,1),14 % 1 = 0.可以整除,所以第14位存储a
5、根据内存对齐原则三:
最后结构体的内存大小
必须是结构体中最大成员即double b(8)内存大小
的整数倍,不足的需要补齐,所以sizeof(SZstruct2)的结果是16
SZstruct3内存大小计算详细过程
1、char a: 从第0位开始,占1个字节,根据上面对齐原则一的公式min(0,1),0 % 1 = 0,可以整除,即第0位存储a;
2、short d:从第1位开始,占2个字节,此时min(1,2),1 % 2 = 1,不能整除,所以位数向后移动直到第2位min(2,2),2 % 2 = 0,可以整除,所以2~3存储d;
3、int c:从第4位开始,占4个字节,此时min(4,4),4 % 4 = 0,可以整除,所以4~7存储c;
4、double b
:从8位开始,占8个字节,此时min(8,8),8 % 8 = 0.可以整除,所以8~15存储b
5、SZstruct2 str2:从第16位开始,根据内存对齐原则二:
str2自身长度为8,占8个字节,此时min(16,8),16 % 8 = 0.可以整除,所以16~31存储str2
6、根据内存对齐原则三:
最后结构体的内存大小
必须是结构体中最大成员即str2(8)内存大小
的整数倍,不足的需要补齐,所以sizeof(SZstruct2)的结果是32
如果上面的文字描述的不够清楚,可以看下面的图解:
总结:结构体内存大小与结构体成员内存大小的顺序有关 。
那么字节对齐到底采用多少字节对齐?
class_getInstanceSize
:是采用8字节对齐
,参照的对象的属性内存大小
malloc_size
:采用16字节对齐
,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍
为什么分配内存采用16字节对齐呢?
(1).通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块
为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数
来降低cpu的开销,这就是以空间换时间。
(2).16字节对齐,是由于在一个对象中,第一个属性isa
占8
字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
小结:这里也就解释了上一篇文章iOS底层源码探究之alloc、init中(cls->instanceSize) 计算需要开辟的内存空间大小
为何是16字节对齐
字节对齐算法(16字节为例)
两种16字节对齐算法:
1、alloc
源码分析中的align16
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
以6+15为例的计算过程:
(1).首先将原始的内存 6
与 size_t(15)
相加,得到 6 + 15 = 21
(2).将 size_t(15)
即 15进行~(取反)
操作,~(取反)
的规则是:1变为0,0变为1
(3).
最后将 21 与 15的取反结果 进行 &(与)
操作,&(与)
的规则是:都是1为1,反之为0
,最后的结果为 16,即内存的大小是以16
的倍数增加的
图解:
2、malloc
源码分析中的segregated_size_to_fit
#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;
}
算法原理:k + 15 >> 4 << 4
,其中先右移4位再左移4位
,相当于 k/16 * 16一样 ,是16字节对齐算法
,小于16就成0了
图解6 + 15:
疑问:既然结构体中成员排序不同会导致其内存大小不一样,那么在OC中,编译后的代码里的结构体里的成员又是怎么排序的呢?编译器是否会对其进行优化呢?请看下一篇文章:iOS底层源码探究之内存优化(属性重排)