OC底层原理02-对象原理-对象内存对齐

571 阅读7分钟

内存对齐

  • 苹果早期以8字节对齐,现在以16字节对齐。
  • isa占8字节 一个对象指针占8字节
  • 对象最小16字节。真实对象大小取决于属性和内存对齐
  • iOS 内存是小端对齐模式

为啥要16字节对齐

  • 方便读取
    • cpu存取数据时,以区块来做范围的读取,如果读取到的资料是没有经过对齐的,资料读取上就要花更多的次数读取完成,意味著增加了cpu读取时的负担。
  • 更加安全.
    • 由于一个对象的本质为结构体,且第一个属性为isa指针(占8个字节),在没有其他属性的状况下,会预留8个字节,其目的是为了数据读取的安全性,如果不预留,有可能造成资料读取混乱。

内存对⻬的原则

在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object的结构体结构体在内存中是连续存放

  • 结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。
  • 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  • 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍.不足的要补⻬。 有点不好理解,下面实例说明:

内存对齐步骤

image.png

struct LGStruct1 {
    double a;   // 8 (0-7)
    char b;     // 1 [8 长度1] (8)
    int c;      // 4 [9 长度4] 9 10 11 (12 13 14 15)
    short d;    // 2 [16 长度2] (16 17)
}struct1;

struct LGStruct2 {
    double a;   //8 (0-7)
    int b;      //4 (8 9 10 11)
    char c;     //1 (12)
    short d;    //2 13 (14 15) - 16
}struct2;
// 15 -> 16

以struct1为例:

  1. 依次计算属性所占大小
    double占8字节,从0-7.
    char占1字节,此时空位到第8位置,8是1(char所占字节)整数倍,符合规则可以落位位置8.
    int占4字节,此时空位到第9位置,但9不是4(int所占字节)的整数倍,往后移位。直到12位置符合规则,int c落位(12 13 14 15).
    short占2字节,此时空位到第16位置,16是2(short所占字节)整数倍,符合规则可以落位,落位(16 17).
    至此,内部所占位置(0-17),大小为18.
  2. 计算所有属性里最大属性所占内存大小
    此例中double最大,为8.
    • 如果属性仍然是结构体 同样方式计算结构体所占大小
  3. 结构体总大小需是内部内存占用最大的整数倍,不足的要补齐
    此例18不是8的整数倍,24是内部最大成员(double)所占内存8,最小的整数倍。所以struct1结构体所占内存为24. 同样步骤struct2是16.
    明明内部类型都一样,大小不一样呢?

系统会做属性重排

image.png 明明内部类型都一样,大小不一样呢?因为苹果针对age、c1、c2属性的内存进行了重排. age类型占4个字节,c1和c2类型char分别占1个字节,通过4+1+1的方式,按照8字节对齐,不足补齐的方式存储在同一块内存中,以此来优化内存.

结构体嵌套

struct Struct1{
    //              占字节数    起始位置     占的位置
    char a; //        1         0           0
    int b;  //        4         4           4,5,6,7
    short c;//        2         8,9
}struct1; //成员最大占4字节,所以struct1占12字节

struct Struct2{
    //                        占字节数    起始位置     占的位置
    int a;                 //  4            0         0,1,2,3
    double b;              //  8            8         8-15
    char c;                //  1            16          16
    struct Struct1 struct1;//  12  struct1最大成员(int)4的大于16的最小倍数是:20   char(20),int(24-27),short(28,29)因为struct1所占内存要是其内部最大成员int整数倍,所以要留出30,31.(20-31是12,是int的整数倍)
    short d;               //  2            32          32,33
}struct2;//成员(包括Struct1里最大成员int占4位)最大是double占8位,所以struct2是8的整数倍,占40

分析

  1. Struct1占其成员最大int4的倍数,为12.
  2. Struct2里的struct1起始位置要参考Struct1里最大成员的倍数,即起始位置是 大于16的 int的最小倍数是:20,所以Struct1里的char从20开始;
    空位 直到是int倍数24时候开始是int b。
    short c占28和29.
    Struct1 struct1从20到29占10,不是最大int倍数,所以Struct1 struct1空位到31,Struct1 struct1共占12字节
  3. short d从32开始,占32和33. 所以Struct2所占内存是内部最大成员占用空间的整数倍,即double(8字节)整数倍,即40字节.

sizeofclass_getInstanceSizemalloc_size

先看示例:

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;//0-7
@property (nonatomic, copy) NSString *nickName;//8-15
@property (nonatomic, assign) int age;//16-19
@property (nonatomic, assign) double height;//24-31
@property (nonatomic) char c1;//32
@property (nonatomic) char c2;//33

@end
LGPerson *person = [LGPerson alloc];
person.name      = @"Cooci";
person.nickName  = @"KC";
person.age       = 18;
person.c1        = 'a';
person.c2        = 'b';
person.height    = 123.5;//double
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size(( __bridge const void *)(person)));
打印结果:<LGPerson: 0x100705090> - 8 - 40 - 48

sizeof

  • sizeof计算类型占用的内存大小,其中可以放 基本数据类型、对象、指针
    • 对于类似于int这样的基本数据而言,sizeof获取的就是数据类型占用的内存大小,不同的数据类型所占用的内存大小是不一样的

    • 而对于类似于NSObject定义的实例对象而言,其对象类型的本质就是一个结构体(即 struct objc_object)的指针,所以sizeof(objc)打印的是对象objc的指针大小,我们知道一个指针的内存大小是8,所以sizeof(objc) 打印是 8。注意:这里的8字节与isa指针一点关系都没有!!!)

    • 对于指针而言,sizeof打印的就是8,因为一个指针的内存大小是8,

class_getInstanceSize

  • class_getInstanceSize 用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小
    • class_getInstanceSize([LGPerson class])为40

      • name String 8 0-7
      • nickName string 8 8-15
      • age int 4 16-19
      • height long 8 24-31
      • 最大属性string整数倍 所以为40
    • 对于一个对象来说,其真正的对齐方式 是 8字节对齐,8字节对齐已经足够满足对象的需求了。

      • class_getInstanceSize:是采用8字节对齐,参照的对象的属性内存大小 可参见objc4-818.2源码如下:
      size_t class_getInstanceSize(Class cls)
      {
          if (!cls) return 0;
          return cls->alignedInstanceSize();
      }
      
      uint32_t alignedInstanceSize() const {
          return word_align(unalignedInstanceSize());
      }
      
      #ifdef __LP64__ //64位
      ...
      #   define WORD_MASK 7UL
      ...
      #else
      //WORD_MASK位 字节对齐算法
      static inline size_t word_align(size_t x) {
          return (x + WORD_MASK) & ~WORD_MASK; //64位8字节对齐
      }
      
    • apple系统为了防止一切的容错,采用的是16字节对齐的内存,主要是因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。

malloc_size

  • malloc_size这个函数是获取系统实际分配的内存大小
    • 此例返回32
    • malloc_size:采用16字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍
      #define SHIFT_NANO_QUANTUM 4
      #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
      // 16字节对齐
      static MALLOC_INLINE size_t segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
      {
         //...
         // (size + 15) >> 4 << 4 16的倍数 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
         ...
         return slot_bytes;
      }
      

其他

苹果内存对齐算法

目前已知的16字节内存对齐算法有两种,在OC对象原理alloc和init源码分析有提及。

align16

   static inline size_t align16(size_t x) 
   { //16字节对齐 16的倍数
     return (x + size_t(15)) & ~size_t(15);
   }

segregated_size_to_fit

#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

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

文章列表