OC底层原理

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

2019-12-18  本文已影响0人  只写Bug程序猿

内存对齐规则

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

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

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

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

image.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做的内存对齐优化,节省了内存开销
数据类型所占字节
C OC 32位 64位
bool BOOL(64位) 1 1
signed char (_ _signed char)int8_t、BOOL(32位) 1 1
unsigned char Boolean 1 1
short int16_t 2 2
unsigned short unichar 2 2
int int32_t、 NSInteger(32位)、boolean_t(32位) 4 4
unsigned int boolean_t(64位)、NSUInteger(32位) 4 4
long NSInteger(64位) 4 8
unsigned long NSUInteger(64位) 4 8
long long int64_t 8 8
float CGFloat(32位) 4 4
double CGFloat(64位) 8 8

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打印

image.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

image.png
继续全局搜索nano_calloc
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);

点击进去


image.png

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

calloc流程.jpg
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字节对齐

上一篇下一篇

猜你喜欢

热点阅读