怎么查看内存
- 通过sizeof可以获取基本数据类型的内存占用,一般用于查看栈空间中基本数据类型内存情况:
// 1. 基本数据:
NSLog(@"BOOL:%lu",sizeof(BOOL)); // BOOL:1
NSLog(@"short:%lu",sizeof(short)); // short:2
NSLog(@"int:%lu",sizeof(int)); // int:4
NSLog(@"long:%lu",sizeof(long)); // long:8
NSLog(@"float:%lu",sizeof(float)); // float:4
NSLog(@"double:%lu",sizeof(double)); // double:8
// 2.结构体数据:
struct TheStructOne{
int one;
}TheStructOne;
struct TheStructTwo{
int one;
int two;
}TheStructTwo;
struct TheStructThree{
int one;
int two;
int three;
}TheStructThree;
NSLog(@"TheStructOne:%lu",sizeof(TheStructOne)); // TheStructOne:4
NSLog(@"TheStructTwo:%lu",sizeof(TheStructTwo)); // TheStructTwo:8
NSLog(@"TheStructThree:%lu",sizeof(TheStructThree)); // TheStructThree:12
- 通过class_getInstanceSize方法可以对象在堆空间中实际占用的内存情况,通过malloc_size可以获取实际为对象开辟的内存空间情况,这两者都只对对象有效,一般用于查看堆空间中对象内存情况:
// 0. 定义一个函数打印内存占用情况
void printMemory(NSObject *objc)
{
NSLog(@"类型:%@->类型占用,实际占用,实际分配:%lu %lu %lu",objc.class,sizeof(objc),class_getInstanceSize([objc class]),malloc_size((__bridge const void*)(objc)));
}
// 1.基类内存情况
printMemory([NSObject alloc]); // 类型:NSObject->类型占用,实际占用,实际分配:8 8 16
// 2.自定义类内存情况
@interface TheObjectOne : NSObject
{
int one; // 4
}
@end
@interface TheObjectTwo : NSObject
{
int one; // 4
int two; // 4
}
@end
@interface TheObjectThree : NSObject
{
int one; // 4
int two; // 4
int three; // 4
}
@end
printMemory([TheObjectOne alloc]); // 类型:TheObjectOne->类型占用,实际占用,实际分配:8 16 16
printMemory([TheObjectTwo alloc]); // 类型:TheObjectTwo->类型占用,实际占用,实际分配:8 16 16
printMemory([TheObjectThree alloc]); // 类型:TheObjectThree->类型占用,实际占用,实际分配:8 24 32
printMemory([TheObjectFour alloc]); // 类型:TheObjectFour->类型占用,实际占用,实际分配:8 24 32
printMemory([TheObjectFiv alloc]); // 类型:TheObjectFiv->类型占用,实际占用,实际分配:8 32 32
printMemory([TheObjectSix alloc]); // 类型:TheObjectSix->类型占用,实际占用,实际分配:8 32 32
printMemory([TheObjectSeven alloc]); // 类型:TheObjectSeven->类型占用,实际占用,实际分配:8 40 48
printMemory([TheObjectEight alloc]); // 类型:TheObjectEight->类型占用,实际占用,实际分配:8 40 48
printMemory([TheObjectNine alloc]); // 类型:TheObjectNine->类型占用,实际占用,实际分配:8 48 48
printMemory([TheObjectTen alloc]); // 类型:TheObjectTen->类型占用,实际占用,实际分配:8 48 48
内存怎么是这样的
好像有的结论
- 结构体实际占用空间大小是各基础数据类型的总大小之和。
- 对象类型恒定占用为8,实际占用空间最低8,实际分配最低16。而实际占用空间以8为跨度增长,实际分配空间最低以16为跨度增长。
那么是不是呢?
结构体占用大小
我们看看结构体,将结构体数据类型换成其它基础类型:
struct TheStructCharOne{
char one;
}TheStructCharOne;
struct TheStructCharTwo{
char one;
int two;
}TheStructCharTwo;
struct TheStructCharThree{
char one;
int two;
int three;
};
NSLog(@"%lu",sizeof(TheStructCharOne)); // 1
NSLog(@"%lu",sizeof(TheStructCharTwo)); // 8
NSLog(@"%lu",sizeof(TheStructCharThree)); // 12
和我们想的不一样,大小从1突然就到8了,有点像类实例分配时候增长的跨度。
类占用大小
首先通过命令 clang -rewrite-objc main.m -o main.cpp
将main.m编译为C++,为了方便查看
我们将编译后的main.cpp拖入到xcode工程中,然后为了解除编译报错,从编译源码中移除:
我们可以看到编译后的TheObjectThree变成了:
struct NSObject_IMPL {
Class isa; // 8
};
struct TheObjectThree_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int one; // 4
int two; // 4
int three; // 4
};
结构体包含结构体等价于把子结构体的所有成员全部放入父结构体中,所以相当于:
struct TheObjectThree_IMPL {
struct NSObject_IMPL {
Class isa; // 8
};
int one; // 4
int two; // 4
int three; // 4
};
可以看出来类的本质是结构体,只不过自定义类相比普通结构体都会多出一个8字节的isa成员。所以搞清楚了结构体为什么实际分配内存空间比成员结构实际占用空间计算出来的大,就能搞清楚类的相同情况。
内存对齐是什么
我们先想一想计算机是怎么读取内存的。不同的处理器根据处理能力不同会一次性读取固定位数的内存块,这个我们称之为内存存取粒度。
我们知道了内存是按块来读取的,现在假设一个结构的大小刚好在一个内存块的大小范围内,理想情况下只需要一次就能读取成功,但如果它的起始位置在上一个内存块,结束在另一个块,那么这个CPU只能读取两次,带来了存取效率上的损失,而且中间还会做剔除和合并。所以为了提高效率及方便读取,我们需要将这个内存进行对齐,使其能在最小读取次数内进行访问。
对于某些架构的CPU甚至会发生变量不对齐就报错,这种情况下只能保证能存对齐。
对齐规则
内存对齐原则
- 数据成员对齐原则: 结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小.
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.
- 结构体的总大小,也就是sizeof的结果,必须是其内部最大 成员的整数倍,不足的要补⻬.
总之,内存对齐原则就是:min(m,n) //m为开始的位置,n为所占位数。当m是n的整数倍时,条件满足;否则m位空余,m+1,继续min算法:
如MyStruct1实际占用计算过程:
a:从0开始,此时min(0,1),即[0]存储a
b:从1开始,此时min(1,8),1%8!=0,继续往后移动,直到min(8,8),即[8-15]存储b
c:从16开始,此时min(16,4),16%4=0,即[16-19]存储c
d:从20开始,此时min(20, 2),20%2=0,即[20-21]存储d
通过这一步骤,我们可以做一些优化重排的工作:
struct Optimize1 {
double a; // 8
int b; // 4
bool c; // 1
}Optimize1;
/*
min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
实际大小为13bytes,最大变量的a字节数为8,最小的满足8的整数倍的是16,Optimize1分配内存为16bytes.
*/
struct Optimize2 {
int d; // 4
double e; // 8
bool f; // 1
}Optimize2;
/*
min(0,4)=d=>[0-3],min(4,8)=e=>[8-15],min(16, 1)=f=>[16];
实际大小为17bytes,最大变量的a字节数为8,最小的满足8的整数倍的是24,Optimize2分配内存为24bytes.
*/
这里我们想到了类的结构体嵌套,试着根据规则计算结构体嵌套:
struct Optimize3 {
double a; // 8
int b; // 4
bool c; // 1
struct Optimize2 opt2; // 24
}Optimize3; // 结构体大小:40,结构体成员大小opt2:24
/*
min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
min(16,24)=opt2=>[16,40]
*/
struct Optimize4 {
struct Optimize2 opt2; // 24
double a; // 8
int b; // 4
bool c; // 1
}Optimize4; // 结构体大小:40,结构体成员大小opt2:24
/*
min(0,24)=opt2=>[0,23];
min(24,8)=a=>[24-31],min(32,4)=b=>[32-35],min(36, 1)=c=>[36]; // 40
*/
struct Optimize5 {
double a; // 8
int b; // 4
bool c; // 1
int d; // 4
double e; // 8
bool f; // 1
}Optimize5; // 结构体大小:40
/*
min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
min(13,4)=d=>[16-19],min(20,8)=e=>[24-31],min(32, 1)=f=>[32]; // 40
*/
可以看出来结构体的嵌套等同于将从子结构体中将成员变量直接放到父成员变量中,所以TheObjectOne_IMPL结构体等价与:
struct TheObjectOne_IMPL {
Class isa; // 8
int one; // 4
}; // 16
根据以上规则,NSObject类对应的NSObject_IMPL结构体对应的类型占用/实际占用/实际分配应该为8/8/8,为何NSObject最后打印出来的结果是8/16/16呢?通过OC源码,我们窥探一下:
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());
}
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
Id define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
可以看到,class_getInstanceSize就是获取实例对象中成员变量的内存大小。
而(x + WORD_MASK) & ~WORD_MASK
相当于(x+7) & ~7
:
8的二进制 0000 1000 后三位都是0
7的二进制 0000 0111 后三位都是1
~7的后三位都是000,经过&运算后三位一定是000,最后的结果必定是8的倍数.
(x+7)的含义是任意一个数给你最大的可能性升阶(8的n阶乘).
如1+7=8,8的一阶.11+7=18.就是8的2阶2*8=16.相比16相差2,所以后三位的就不管了直接&运算就抹零了.
所以class_getInstanceSize最小返回为16.根据16字节对齐规则我们可以推断出类的起始地址以0开始。
我想影响内存对齐
这里有两个编译指令:
Idpragma pack(n)
n就是你要指定的“对齐系数”,一次性可以从内存中读/写n个字节.n=1,2,4,8,16.
Idpragma pack()
取消自定义字节对齐.
__attribute__((aligned (n)))
让所作用的结构成员对齐在n字节自然边界上.
__attribute__((packed))
取消优化对齐,按照实际占用字节数对齐.
内存对齐的"对齐数"取决于"对齐系数"和"成员的字节数"两者之中的较小值。