02 - 探究内存对齐

309 阅读5分钟

探究内存对齐

回顾:上一章节,我去探究了对象的alloc流程,以及initnew的区别,感兴趣的同学可以去查看我上一章内容。


任务: 上章节中, 在alloc中计算对象所占内存空间时, 有说到字节对齐的知识点, 今天咱们重点来探索它.

准备工作: 
	po: "expression -O"的简写, 作用打印对象.
    p : "expression --"的缩写, 打印返回值的类型以及命令结果的引用名。
    x : 用16进制来打印对象内存数据
    x/4gx: 格式化打印对象内存数据
    bt: 打印当前堆栈信息

- 探索对象属性内存对齐

我们先来看这段代码:

    LLPerson *person = [LLPerson alloc];
    person.name      = @"luln4";
    person.nickName  = @"LL";
    person.age       = 28;
    person.c1        = 'a';
    person.c2        = 'b';
    
    NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size((__bridge const void *)(person)));

先分析person对象占据了多大内存:

isa: 8个字节, 继承NSObjce而来
name: 8个字节, NSString属性
nickName: 8个自己, NSString属性
age: 4个字节, Int属性
c1: 1个字节, char属性
c2: 1个字节, char属性

我们假设没内存对齐, 那person对象实际占用内存应该是30个字节, 接下来我们来看打印结果:

<LLPerson: 0x103a081e0> - 8 - 32 - 32

为什么呢? 我们断点到NSLog这里, 并在lldb里打印它的内存数据看下 x/4gx 0x103a081e0

(lldb) x/4gx 0x103a081e0
0x6000015815c0: 0x000000010e104778 0x0000001200006261
0x6000015815d0: 0x000000010e102038 0x000000010e102058

我们分别po这些属性, 可以看到0x000000010e104778, 0x000000010e102038, 0x000000010e102058分别打印出LLPerson, luin4, LL, 但比较奇怪的就是属性中的age和c1, c2去哪了?

po 0x0000001200006261是乱码. 我们仔细观察0x0000001200006261, age是4个字节, c1, c2分别是1个字节, 那我们是不是应该分开你去打印呢:

po 0x00000012 	/// 28
po 0x62 		/// 98 在ASCII码中: b
po 0x61			/// 97 在ASCII码中: a

总结: 看来系统不仅在alloc流程中计算内存大小的源码中, 16位字节对齐(详见size = cls->instanceSize(extraBytes)), 还对内存空间进行了优化 -> 重排 ,大大节省了空间, 不然每个属性都字节对齐占用8个字节, 那得浪费多少内存!


- 探索结构体内存对齐

  • 准备工作1 -> 打印内存的三种方式
	-  sizeof() 
	-  class_getInstanceSize(类对象)
    -  malloc_size((__bridge const void *)(指针)))
  • 准备工作2 -> 各个类型所占空间

sizeof():

1sizeof是一个运算符, 不是函数
2、我们一般用sizeof计算内存大小时,传入的主要对象是数据类型,这个在编译器的编译阶段(即编译时)就会确定大小而不是在运行时确定。
3sizeof最终得到的结果是该数据类型占用空间的大小

class_getInstanceSize

runtime API, 计算并返回该对象实际所需空间大小

malloc_size

获取系统实际分配的内存大小(可以结合16字节对齐算法来分析其返回值)

先定义一个结构体

  struct LLStruct1 {
      int a;  	/// 4 
      char b;   /// 1 
      long c;   /// 8 
      short d;  /// 2 
  }struct1;

如上述结构体示例, 根据准备工作2图片, 我给每个成员注释了它所占的空间, 那是不是说这个结构体只需要 15 -> 16 个字节就可以了, 我们来打印看一下:

NSLog(@"%lu",sizeof(struct1)); 输出结果: 24

  struct LLStruct1 {
      int a;  	/// 4 0-3
      char b;   /// 1 4
      long c;   /// 8 [5,6,7] 8-15
      short d;  /// 2 16 17
  }struct1;

我们先大概猜测一下结构体的<内存对齐>规则:

	1: 起始位 是该成员所需内存大小的整倍数;
    2: 基于第1点计算出的实际内存大小应转换为 <该结构体><占用空间最大> 的成员变量的整倍数

我们来验证下:

如图所示, 规则是成立的, 那有人说了, 如果结构体中嵌套了结构体呢? 难道要先计算出 结构体成员 的分配大小, 再来计算该结构体的分配大小呢? 上代码

  struct LLStruct4 { 
      double a; /// 8
      int b;	/// 4
      char c;	/// 1
      short d;	/// 2
  }struct4;
  ///系统给struct4共分配16字节
  
  struct LLStruct3 {
      int a;
      char b;
      long c;
      short d;

      struct LLStruct4 e;
  }struct3;
  
  NSLog(@"%lu",sizeof(struct3)) /// 40

接上图继续画:

e: (16字节) 起始偏移量应为8(struct4 -> a, 结构体内最大成员的大小)的倍数, [24 - 39] -> 40(struct3中最大成员为long和结构体指针, 都为8字节, 所以应为8的倍数)

结构体的<内存对齐>规则总结:

	1: 起始位 是该成员所需内存大小的整倍数;
    2: 基于第1点计算出的实际内存大小应转换为 <该结构体> 中 <占用空间最大> 的成员变量的整倍数;
    3: 当结构体a内嵌套结构体b时, b所存储地址起始偏移位应为b内最大成员大小的整倍数;

最后, 为什么要内存对齐呢?

	1: 方便快捷
    > 如果不对齐, 那么就会动态自适应读取内存的长度, 这时候消耗大量性能和时间去计算和适配.
    2: 安全
    > 如果不对齐, 并且没有自适应读取内存, 那么就会出现访问到其他对象的情况,甚至会出现访问到野指针的情况.
* 对齐后, 系统只需要固定读取长度, 以**空间换时间**来读取对象的内存, 就会大大提高访问速度以及安全
* 注意区分x86和64位系统所占空间内存