iOS 底层探索篇 —— 内存字节对齐分析|8月更文挑战

711

LLVM拦截优化

上一篇说到alloc流程,第一步应该是alloc放法,那么实际上是不是这样呢?我们验证一下: 我们在alloc的地方打个断点,运行一下:

在这里插入图片描述

打开汇编: 在这里插入图片描述

发现这里实际上走的是objc_alloc,这是为什么呢? 我们搜索一下objc_alloc,发现在objc-runtime-new.mm文件中发现这样一个fixupMessageRef函数,发现当msgSEL等于alloc的时候,就会把msg的实现改为objc_alloc.

在这里插入图片描述

寻找一下是哪里调用的fixupMessageRef,发现是在_read_images方法里面。

在这里插入图片描述

我们看到,fixupMessageRef是有问题的时候,才去修改,那么alloc没有问题的时候为什么也是调用objc_alloc呢?如果在readImage之前,那么说明可能是在编译的时候就做了处理,所以这里去看一下llvm源码。 接下来在llvm源码中搜索objc_alloc,看到了这样一段注释:

在这里插入图片描述

就是当这个方法返回true的时候,alloc会被改为objc_alloc, allocWithZone:nil 会被改为objc_allocWithZone。 来看一下这个方法,得知什么时候会返回true,发现是用version来进行判断的。

在这里插入图片描述

继续往下看,发现了这样一段代码。

在这里插入图片描述

接下来寻找EmitObjCAlloc.

在这里插入图片描述

到这里我们明白了,这里如果发现了SEL的名字为alloc,那么就会调用CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));方法,将方法实现改为objc_alloc。在看一下这个方法是什么时候被调用的。搜索一下tryGenerateSpecializedMessageSend

在这里插入图片描述

发现是在GeneratePossiblySpecializedMessageSend里面。 我们现在知道,苹果对alloc等一些方法在编译阶段LLVM会对这些方法进行处理,比如alloc,因为他们想要监控对内存的开辟等关键,所以会对这些方法进行hook处理,当运行alloc的时候,会先运行objc_alloc,然后进行下层的标记,标记完成后再去执行alloc方法,然后就进行正常的alloc方法流程。

一.获取内存大小的三种方式

  1. sizeof
  2. class_getInstanceSize
  3. malloc_size

1. sizeof

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

2. class_getInstanceSize

是runtime提供的api,用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小。8字节对齐。

3. malloc_size

这个函数是获取系统实际分配的内存大小。实际分配的和实际占用的内存大小并不相等,对象申请的内存空间 <= 系统开辟的内存空间。因为对象申请的内存空间是以8字节对齐方式。在objc源码里面是可以得到验证的。而系统开辟内存空间是以16字节对齐方式。在malloc源码里segregated_size_to_fit()可以看到是以16字节对齐的。对于一个对象来说,其真正的对齐方式 是 8字节对齐,8字节对齐已经足够满足对象的需求了,apple系统为了防止一切的容错,采用的是16字节对齐的内存,主要是因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。

二. 内存对齐原则

  1. 数据成员的对齐规则可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即 m % n == 0), n 从 m 位置开始存储, 反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。

  2. 数据成员为结构体:当结构体嵌套了结构体时,作为数据成员的结构体的自身长度作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度为8

  3. 最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐。

结构体内存对齐

三. 结构体内存分析

1. 结构体1

struct LGStruct1 { double a;
char b;
int c;
short d;
}struct1;

在这里插入图片描述

结构体LGStruct1 内存大小计算

  • 变量a:占8个字节,从0开始,此时min(0,8),即 0-7 存储 a
  • 变量b:占1个字节,从8开始,此时min(8,1),即 8 存储 b
  • 变量c:占4个字节,从9开始,此时min(9,4),9不能整除4,继续往后移动,知道min(12,4),从12开始,即 12-15 存储 c
  • 变量d:占2个字节,从16开始,此时min(16,2),即 16-17 存储 d

根据内存对齐规则得出WJStruct1的内存大小是18 ,但是18不是最大变量的字节数8的整数倍,18向上取整到24,主要是因为24是8的整数倍,所以 sizeof(struct1) 的结果是 24

在这里插入图片描述

在这里插入图片描述

2. 结构体2

struct LGStruct2 { double a;
int b;
char c;
short d;
}struct2;

在这里插入图片描述

结构体LGStruct2 内存大小计算

  • 变量a:占8个字节,从0开始,此时min(0,8),即 0-7 存储 a
  • 变量b:占4个字节,从8开始,此时min(8,4),即 8 - 11 存储 b
  • 变量c:占1个字节,从12开始,此时min(12,1),即 12 储存c
  • 变量d:占2个字节,从13开始,此时min(13,2),13不能整除2,继续往后移动,直到min(14,8),从14开始,即 14-15 存储 c

根据内存对齐规则得出WJStruct2的内存大小是16 ,16刚好是8的整数倍,所以sizeof(struct2) 的结果是 16

在这里插入图片描述 在这里插入图片描述

3. 结构体3

// 家庭作业 : 结构体内存对齐 struct LGStruct3 { double a;
int b;
char c;
short d;
int e;
struct LGStruct1 str; }struct3;

在这里插入图片描述

  • 变量a:占8个字节,从0开始,此时min(0,8),即 0-7 存储 a
  • 变量b:占4个字节,从8开始,此时min(8,4),即 8 - 11 存储 b
  • 变量c:占1个字节,从12开始,此时min(12,1),即 12 储存c
  • 变量d:占2个字节,从13开始,此时min(13,2),13不能整除2,继续往后移动,直到min(14,8),从14开始,即 14-15 存储 c
  • 变量e:占4个字节,从16开始,此时min(16,4),即 16-19 存储 a
  • 结构体成员str: str 是一个结构体,根据内存对齐原则,结构体成员要从其内部最大成员大小的整数倍开始存储,而LGStruct1中最大的成员大小为8,所以str要从8的整数倍开始,此时min(20,8)20不能整除8,继续往后移动,直到min(24,8),24是8的整数倍,符合内存对齐原则,所以 24-47 存储 str 因此LGStruct3的需要的内存大小为 48 字节,而 LGStruct3 中最大变量为str, 其最大成员内存字节数为8,根据内存对齐原则,所以 LGStruct3 实际的内存大小必须是 8 的整数倍,48正好是8的整数倍,所以 sizeof(LGStruct3) 的结果是 48

在这里插入图片描述

在这里插入图片描述