目前但凡一个iOS岗面试都会问个内存对齐问题,那么什么是字节对齐?成员变量对齐和对象内存对齐有什么区别?今天我来为大家一一解析
iOS创建对象的_class_createInstanceFromZone
方法中会通过instanceSize
方法计算创建对象需要开辟的内存空间(细创建对象流程点我),源码如下:
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
alignedInstanceSize()源码如下:
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
通过分析源码得出以下结论:
- 对象开辟的空间大小完全取决于对象的成员变量(depending on class's ivars),通过成员变量得出未对齐的内存大小:
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
若对象没有成员变量,则属性只有从基类NSObject继承来的isa,isa为一个class类型的结构体指针
Class isa OBJC_ISA_AVAILABILITY;
,因此一个对象最小的未对齐空间unalignedInstanceSize
为8。得到
unalignedInstanceSize
以后,通过word_align
方法将其字节对齐,接下来逐步分析具体成员变量对齐方法
- 对齐方法代码:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
其中WORD_MASK
定义为define WORD_MASK 7UL
- 假设当前对象没有成员变量,则x = 8,以上表达式可转化为:
(8 + 7) & ~7
-> 15 & ~7
- 转为二进制则为:
15 : 0000 1111
7 : 0000 0111
~7 : 1111 1000
15 & ~7 : 0000 1000 = 8
一个无成员变量的对象通过对齐得到的alignedInstanceSize
为8。
- 字节对齐算法的意义为8字节对齐,取8的整数倍。目的在于在计算读取成员变量的时候,如果成员变量根据实际类型设定大小,会影响计算机读取速度,但如果统一以8字节为度量衡,可以加快读取速度(以空间换取时间)这个算法也可以用以下方式表达:
(8 + 7) >> 3 << 3 右移3位再左移三位
15 : 0000 1111
右移三位 -> 0000 0001
左移三位 -> 0000 1000 = 8
- 得到
alignedInstanceSize
后,instanceSize
最后会通过if (size < 16) size = 16
条件将小于16的结果全部转为16,至此,一个无成员变量的对象开辟的空间为16。
4.接下来分析对象带有多个成员变量的情况:
@property (nonatomic,copy)NSString *name;
@property (nonatomic,assign)int age;
@property (nonatomic,assign)long height;
@property (nonatomic,copy)NSString *nickName;
@property (nonatomic,assign)BOOL isMale;
并进行赋值:
p.name = @"FC";
p.age = 18;
p.height = 185;
p.nickName = @"XX";
p.isMale = YES;
在控制台通过x/6gx p 来打印对象p的详细内存情况:
其中第一段内存为对象的isa,第二段内存中,系统将属性age
和属性isMale
两个4字节的属性进行了组合,优化了内存分配,打印结果如图:
5.内存对齐原则:
数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要
从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储。结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从
其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b
⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤
成员的整数倍.不⾜的要补⻬。
举例:
以上两个结构体内存大小是否一致?答案:不一致,分别为24和16。
为什么同样的成员变量,因为顺序不同内存结果不同?
依据原则1”每个成员变量储存的起始位置需要可以整除该成员变量的字节大小“,得出两个结构体的储存流程如下:
再依据原则3,内存结果必须为最大成员的整数倍,得出结果为24和16。
为什么每个成员变量起始位置必须要是自己的整数倍呢?
不做对齐操作时,计算机读取此结构体变量需要读取4次,每次读取大小分别为8,1,2,4,读取次数多,每次读取次数不同,降低读取效率。
内存对齐操作后,可以看到后面3个成员变量组合到了一起,并且系统会告知该组合中含有大小为1,2,4的成员变量,计算机再读取此结构体的时候只需以8字节为单位读取两次,大大提高了读取效率,这就是需要内存对齐的原因啦
6.对象内存最终结果
-
创建一个类:
-
如图赋值并进行打印:
-
打印结果为:
按照之前的分析:
sizeof(p)
结果为8(p为结构体指针,大小为8);
class_getInstanceSize([FCPerson class])
为成员变量大小+isa,结果为40
malloc_size
是什么?为什么等于48?
通过研究malloc源码, 在calloc方法最后调用的segregated_size_to_fit
方法中:
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;
}
size为传入的40,NANO_REGIME_QUANTA_SIZE为16, SHIFT_NANO_QUANTUM为4。
slot_bytes就是返回的最终的对象大小,其算法为: (40 + 16 - 1) >> 4 << 4,即对40进行16字节对齐,最终结果为48。
16字节对齐的原因:若以8字节对齐,64字节以内,可以存放8个8字节对象(8,16,24,32,40,48,56,64),对象之间两两相邻。若以16字节对齐,64字节内可以放4个对象(16,32,48,64),对象两两相邻的次数降低,减少系统误指的概率。另一方面,一个对象的成员变量最小为8(isa),但只含有默认变量isa的对象概率极低,实际使用中绝大部分类都有额外的成员变量,若以8字节对齐则每个对象内存都要做额外计算,因此直接使用16字节对齐会提高效率。