oc中对象的内存对齐方式初探

169 阅读5分钟

为什么要内存对齐?

1、平台原因(移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

内存对齐规则

1:数据成员对⻬规则:结构(struct)(或联合体(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。

2.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(strust a里存有struct b ,b 里有char int double的元素,那b应该从8的整书倍开始存储)

3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要对齐

首先通过一个类的内存段来分析

2711707-cd33a126d1247a64.png x p打印当前对象内存段 iOS为小端模式,内存要倒着读 所以我们用一个命令自动帮我们整理好
x/4xg p 意思就是按照16进制,以4整段打印当前p对象 我们发现OC为我们做了一些优化,我们发现0x0000001200006261这个内存段存储了age,char1,char2,三个属性 我们po 0x00000012 打印18,也就是我们的age属性值 po 0x61 0x62 打印的分别为97 98 也就是我们a和b对应的ASCII码 这就是oc做的内存对齐优化,节省了内存开销

数据类型所占字节

COC32位64位
boolBOOL(64位)11
signed char(_ _signed char)int8_t、BOOL(32位)11
unsigned charBoolean11
shortint16_t22
unsigned shortunichar22
int int32_t、NSInteger(32位)、boolean_t(32位)44
unsigned intboolean_t(64位)、NSUInteger(32位)44
longNSInteger(64位)48
unsigned longNSUInteger(64位)48
long longint64_t88
floatCGFloat(32位)44
doubleCGFloat(64位)88

demo

struct LGStruct1 {
    char a;     // 1 + 7          
    double b;   // 8
    int c;      // 4
    short d;    // 2 + 2
} MyStruct1;

struct LGStruct2 {
    double b;   // 8
    char a;     // 1 + 7
    int c;      // 4 
    short d;    // 2
} MyStruct2;
NSLog(@"%lu-%lu",sizeof(MyStruct1),sizeof(MyStruct2));

//打印结果为24-24

我们在来看一个demo

struct Struct1 {
    int a;              //4 + 4
    double b;           //8
    int c;              // 4
    char d;             //1
    short e;            //2
}myStruct1;

struct Struct2 {
    int a;                  //4 + 4
    double b;               //8
    char d;                 //1 + 7
    int c;                  //4
    short e;                //2
}myStruct2;
 NSLog(@"%lu - %lu",sizeof(myStruct1),sizeof(myStruct2));
//打印结果为24-32

只是属性顺序不一样,为什么最后得到的大小不一样呢 int 为4字节 ,

double为8字节,

char为一字节,

short为2字节

myStruct1中

a占[0-3]的位置

b占[4-15]

c占[16-19]

d占[20-21]

e占[22-23]

所以打印size为24 a为int类型,4字节,下个成员b为double占8字节,根据开头的对齐规则第一条,所以要+4

myStruct2中 char d + 7 是因为a+b= 16字节 则d为第17字节,c为4字节,offset要为4的倍数所以+7 则最后结果为32(最大成员的整数倍)

再来看一个面试经常遇到的坑

 LGTeacher  *p = [LGTeacher alloc];
        
        p.name = @"LG_Cooci";       //8
        p.age  = 18;                  //4 内存对齐 + 4
        p.height = 185;               //8
        p.hobby  = @"女";            //8
        NSLog(@"%@",p);
        // 5 * 8 == 40
        //malloc_size 为48
    //对象字节对齐是以16字节对齐.而属性为8字节对齐
        NSLog(@"%lu--------%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
打印结果为40 ---- ---- 48

这几个属性加起来也才32啊为什么是40 呢, 因为类还有一个isa指针,而isa指针占8位,所以打印为40

malloc为什么是48 呢,还要从源码开始 我嗯可以直接在libmalloc中调用 calloc(1, 40),点进去calloc中可以看到

retval = malloc_zone_calloc(default_zone, num_items, size);

然后在点进去

ptr = zone->calloc(zone, num_items, size);

然后问题来了,在点击 calloc,死循环找不到下一步代码,我们可以看到->,这是一个属性函数,我们可以利用lldb打印

2711707-03aba2b63382b940.png

然后全局搜索default_zone_calloc

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);
}

同样我们打印一下zone->calloc

2711707-2730b7f76ec39be5.png 继续全局搜索nano_calloc

void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);

点击进去

2711707-94c9b6cbc68be1c8.png

点击segregated_size_to_fit进去 为了方便理解记忆,奉上一个流程分析图

2711707-63263875c066ce46.png

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
	// size = 40
	size_t k, slot_bytes;

	if (0 == size) {
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	// 40 + 16-1 >> 4 << 4
	// 40 - 16*3 = 48

	//
	// 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;
}

一个内存对齐的算法,传进来的size经过算法计算之后必为16的倍数,40不为16的倍数,所以+ 8 = 48 为16的证书倍 +

算法原理同下 看一个简单一点的算法

#   define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    // 7+8 = 15
    // 0000 1111
    // 0000 1000
    //&
    // 1111 1000 ~7
    // 0000 1000 8
    
    // 0000 0111
    //
    // x + 7
    // 8
    // 8 二阶
    // (x + 7) >> 3 << 3
    return (x + WORD_MASK) & ~WORD_MASK;
}

比如我们传进来的是8 8 + 7 = 15 15转化为2进制 0000 1111

7转化为二进制 0000 0111 取非 1111 1000 &0000 1111 =0000 1000 转换为10进制为 8

综上所述,可以得出 1.对象申请的空间(40)和系统开辟的空间(48)是不一样的 2.对象字节对齐是16字节对齐,而属性是8字节对齐