OC对象内存占用及优化
结构体内存对齐原理
前言:我们都知道,在iOS开发中,我们写的oc代码,底层都是用c++来实现的,而oc对象本质就是结构体指针,那么结构体占用内存的计算方法是什么呢,有没有什么规则呢,下面我们就来研究一下。
首先,我们看下面两个结构体,并且打印两个结构体占用的内存大小,看看结果如何。
struct Struct1 {
double a; // 8字节
char b; // 1字节
int c; // 4字节
short d; // 2字节
} struct1;
struct Struct2 {
double a; // 8字节
int b; // 4字节
char c; // 1字节
short d; // 2字节
} struct2;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"struct1 size : %lu \n struct2 size : %lu", sizeof(struct1), sizeof(struct2));
}
我们看到,两个结构体成员类型都是一样的,只是顺序不一样,他们占用内存是不是相同呢?看结果:
image
这结果真是让我们大吃一斤!那为什么顺序不同结果就不一样呢?我们看一下结构体内存对齐原则:
- 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要
从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,
结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存
储。 - 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b
里有char,int ,double等元素,那b应该从8的整数倍开始存储.) - 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补⻬。
看完了对齐原理,我们来验证下为什么刚才的结果是不一样的。
struct Struct1 {
double a; // 8字节 [0...7]
char b; // 1字节 [8]
int c; // 4字节 (9,10,11,[12...15]
short d; // 2字节 [16,17]
} struct1; // 8字节内存对齐 18 -> 24
struct Struct2 {
double a; // 8字节 [0...7]
int b; // 4字节 [8...11]
char c; // 1字节 [12]
short d; // 2字节 (13,[14,15]
} struct2; // 8字节内存对齐 16 -> 16
按照刚才的原理,我们看到确实是这样。接下来我们加大难度:
struct Struct3 {
double a; // 8字节 [0...7]
int b; // 4字节 [8...11]
char c; // 1字节 [12]
short d; // 2字节 (13,[14,15]
int e; // 4字节 [16...19]
struct Struct1 s1; // 24字节 (20,21,22,23,[24...47]
}struct3; // 8字节内存对齐 48 -> 48
如果有结构体嵌套,根据上面的规则,我们计算struct3
内存大小应该是48字节,我们打印下验证结果:
我们再看一种情况
struct Struct4 {
char a; // 1字节 [0]
short b; // 2字节 [2,3]
double c; // 8字节 [8...15]
int d; // 4字节 [16...19]
} struct4; // 8字节内存对齐 20 -> 24
struct Struct5 {
int a; // 4字节 [0...3]
int b; // 4字节 [4...7]
struct Struct4 s4; // 24字节 [8...31]
short c; // 1字节 [32]
}struct5; // 8字节内存对齐 33 -> 40
struct Struct6 {
int a1; // 4字节 [0...3]
int b1; // 4字节 [4...7]
char a; // 1字节 [8]
short b; // 2字节 [10,11]
double c; // 8字节 [16...23]
int d; // 4字节 [24...27]
short e; // 1字节 [28]
}struct6; // 8字节内存对齐 28 -> 32
image
C++结构体是可以继承的,那么struct5
和struct6
却不一样,因为在继承的时候,可以理解成把父结构体这个小组织继承过来,他里面的内存分配形式不变,就算里面有多余的没有用到的内存,子结构体也没有权限去往里面写数据,所以他们的内存占用不同。
既然结构体继承是这样的,那么我们试一下OC中的类呢。
OC中类本质就是结构体指针,那
SJFather
占16字节(isa->8字节,a->1字节,16字节对齐),SJSon
继承SJFather
,如果按上面结构体情况,是不是先把SJFather
的16字节继承过来且没权限修改,再加上一个b->1字节,16字节对齐后占32字节。但是我们看到打印出来16字节,也就是在底层,SJSon
直接把SJFather
成员变量放在自己的结构体中,并没有结构体嵌套,所以SJSon
占用的内存:isa->8 + a->1 + b->1 = 10,16字节对齐后16字节,这里需要注意下。
OC对象内存大小
下面我们来研究下对象的内存大小。
@interface SJPerson : NSObject
@property (nonatomic, copy) NSString *name; // 8
@property (nonatomic, copy) NSString *nickName; // 8
@property (nonatomic, assign) int age; // 4
@property (nonatomic, assign) long height; // 8
@end
SJPerson *sj = [[SJPerson alloc] init];
NSLog(@"%@ - %lu - %lu - %lu", sj, sizeof(sj), class_getInstanceSize([SJPerson class]), malloc_size((__bridge const void *)(sj)));
image
指针8字节,根据上面结构体内存,我们可算出成员变量内存对齐后占用28 -> 32字节,加上isa指针8字节,共40字节,SJPerson
这个类占用40字节就够了,为什么malloc_size
打印出来是48呢,我们研究下。
找到malloc源码,看下calloc流程有哪些。
- _malloc_zone_calloc
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
- 我们根据返回值ptr,找到关键信息
zone->calloc
_malloc_zone_calloc
但是我们点calloc进去
void *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
什么信息都看不到,我们看源码calloc
有好多calloc = xxx
赋值的地方,有赋值的地方就有存储值的地方。
我们可以在zone->calloc
打个断点,当执行到这行代码时,在控制台po zone->calloc
,就会发现输出default_zone_calloc
,我们在全局搜索。
或者用汇编,也可以看到走到default_zone_calloc
方法。
- default_zone_calloc
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
返回值同样看不到任何信息,我们打断点故技重施,会输出nano_calloc
- nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
/// 返回null不用看,我们肯定要找成功返回
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
/// 重要信息
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
/// 当total_bytes大于256,执行下面代码,需要再验证下
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
- _nano_malloc_check_clear
_nano_malloc_check_clear
找到最关键的代码,segregated_next_block
就是死循环查找合适内存空间。 -
segregated_next_block
segregated_next_block
总结下calloc流程图如下:
calloc流程.jpg
calloc流程基本走完了,但是我们最关心的问题,申请空间申请多大呢?我们再回到第5步中,slot_bytes
这个字段即开辟内存空间大小。再网上看这个值咋么获取的
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
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 = 16
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
/// (size + 15) >> 4 << 4,即k为大于size的最小的16字节对齐数据
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
是40的时候,malloc_size
是48了,因为要16字节对齐。那为什么要以16字节对齐呢,oc中成员变量最多的占8字节,或者说为什么不以32字节对齐或者其他。因为如果以8字节对齐,不同对象内存空间是连续挨在一起的,访问时有可能会发生错误,也就是野指针访问,如果扩大到16,内存连续的可能性会降低,一个NSObject
对象只有一个isa
指针,占8字节,空8字节,发生访问错误的几率就会降低,而且随便加一个成员变量,内存就会大于8,如果以8字节对齐,计算量会变大。为什么不用更大32呢?32的话可能会浪费很多内存,所以综上考虑,iOS对象内存空间用16字节对齐。
OC对象内存优化
SJPerson打印看下
sj
的内存分配
内存分配
可以看出系统自动帮我们做了内存分配优化,而且我们写的属性的顺序与内存位置顺序无关。