上篇文章我们发现结构体内部成员变量的顺序会对结构体的内存分配产生影响,接下来我们探究一下对象的内存分布
1. 对象的内存分布和影响因素
首先创建一个类并添加一些属性:
@interface User : NSObject
@property (nonatomic, copy) NSString *id;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) short weight;
@end
打印一下 user 对象的内存分配大小,注意要导入malloc/malloc.h
系统库:
User *user = [User new];
user.id = @"10000";
user.name = @"Gin";
user.age = 18;
user.height = 189.99;
user.weight = 158;
NSLog(@"%lu", malloc_size((__bridge const void *)(user)));
上篇文章我们知道对象内存储的是isa指针 + 成员变量的值,也就是说即使为类添加一些方法也不会对类创建的对象的内存分配大小产生影响,验证如下:
思考:对象存储在堆区,方法以二进制形式存在代码区
上篇文章我们知道结构体内部成员变量的顺序会对结构体的内存分配产生影响,那么我们来探究一下属性的书写顺序和赋值顺序对对象的内存分配有没有影响。
先用lldb调试打印6个8字节的user对象的内存地址
第一个8字节内存地址存储的是isa指针,也就是说从第二个8字节地址开始才是存储对象的成员变量的值,分别打印一下:
发现第二个地址打印的值并不是 id,而 age 和 weight 的值并没有发现,我们试着把第二个地址拆开打印:
发现 age 和 weight 的值出现了,而 age 和 weight 放在了同一个 8 字节内存地址中。至此我们发现了对象的成员变量的值在存储的时候被自动重排了顺序,不受书写顺序和赋值顺序的影响。系统这样做的目的是优化内存。对象内存按 8 字节对齐,int 类型的 age 占 4 字节,short 类型的 weigh 占 2 个字节,用同一个 8 字节内存存储是可行的,如过再加一个 char 类型的 a,总共占用 7 个字节,仍然可以用同一个 8 字节存储,验证如下:
现在我们知道了系统在内存里会对我们对象的属性自动重排顺序,下面再探究一下自己声明的成员变量和继承的属性会不会重排顺序:
从打印结果可以看出,父类自动生成的成员变量并不会和子类自动生成的成员变量一起被系统自动排列,而是各自进行重排。这样做的原因是当子类继承父类的数据结构时,父类是一块连续的内存空间,子类是没有办法去修改父类的数据结构的,也就是说系统在进行属性重排时只是基于某一个类,并不会把子类的成员变量和父类的成员变量混在一起。
2.联合体和位域
以下有两种结构体写法,分别打印一下结构体大小:
我们发现 struct2 的结构体大小只占 1 个字节,struct2 这种写法就是我们常说的位域。也可以节省内存空间
关于位域:
- type name : length ,这种写法位域长度就表示用几个 bit 位存储该成员变量
- 位域的长度不能超过数据类型的最大长度
例如: char 类型的变量最大长度为 8 bit,那么位域标识最大不能超过 8 bit- 一个位域是存储在同一个字节当中的,如果这一个字节所剩的空间不够去存放另一个位域的时候,另一个位域就会从下一个字节开始存放
下面再看一下联合体。
struct User1 {
char *name;
int age;
double height;
}user1;
union User2 {
char *name;
int age;
double height;
int a;
}user2;
union User3 {
char a[7];
int b;
}user3;
user2、user3 为联合体
我们逐行打印断点,发现结构体成员变量在赋值前后都可以看到对应的值,而联合体成员在赋值后,对于类型不同的其他成员的值都是错误的或无效的,而类型相同的成员存储的值是相同的。
我们再打印一下 user2 各个成员变量的的地址:
发现是同一块地址,可以得到以下结论:
- 联合体的成员共用一个内存空间,一次只能使用一个成员
- 联合体可以定义多个成员,访问不同的成员都是访问这个联合体的不同途径
- 对某一个成员赋值,会覆盖其他成员变量
- 可以节省一定的空间
我们再打印一下 user2,user3 的内存大小:
我们发现联合体对于内存的分配:
- 联合体必须能够容纳最大的成员变量
- 通过第一点计算出来的大小必须是其最大成员变量(基本数据类型)的整数倍
我们得出结构体和联合体的区别:
- 联合体的成员变量之间互斥(不能共存);结构体成员变量可以共存
- 联合体内存使用更为精细灵活,也节省了内存空间;结构体的内存开辟比较粗放
3. nonPointerIsa
我们了解联合体和位域之后,再看一下上篇文章所说的_class_createInstanceFromZone
方法内的initInstanceIsa
方法,这个方法是把创建的对象通过对象内的 isa 指针来关联到相应的类,isa 指针内包含了对象所属的类对象的内存地址
进入initInstanceIsa
方法内,我们发现其内部也是调用了initIsa
方法
进入initIsa
方法内,我们发现其内部就是对对象的 isa 指针进行初始化,同时我们发现了 isa_t 数据类型
进入 isa_t 我们发现,isa_t 就是一个联合体,目的是为了兼容以前的版本,现在系统使用的 isa 是 nonPointerIsa,nonPointerIsa 概念的出现也是为了节省内存空间,是空间优化的一种手段。因为对象的 isa 是一个 8 字节的 Class 类型的结构体指针,用于存储类对象的内存地址,就是 class,通过这个地址可以查询到类对象的属性、方法和协议等。
而存储类对象的内存地址不需要使用 8 字节这么大的内存空间,所以系统就把一些与对象息息相关的信息也存储到 isa 的内存空间内,而 nonPointerIsa 的信息度存储在 ISA_BITFIELD
这样一个结构体内
进入ISA_BITFIELD
内,我们发现对于不同架构下的 isa 内存储的成员变量占用的位域长度是不同的:
4. nonPointerIsa 内存储的信息以及如何利用 isa 的位运算找到类对象
从上图我们可以看到 isa 内存储的类对象地址空间在不同架构下分别是 52、33、44
我们在 objc 源码的 main 文件里创建一个 user 对象。我的电脑走的是 arm64 架构。以 arm64 架构为例,计算一下 isa 内存储类对象的内存地址。arm64 架构下 isa 存储类对象地址占用的位域长度是中间的 33 位,低 3 位和高 28 位存储的是其他信息,我们对 isa 指针的地址做** >> 3 << 31 >> 28 ** 的位运算就可以把低 3
位和高 28 位清零,从而得到中间 33 位的类对象的内存地址,验证如下:
可以通过另外一种方式直接得到我们类对象的地址,通过相应架构下的** ISA_MASK 掩码 & 对象的 isa 指针地址 **就可以得到对象的类对象的内存地址
同样我们可以通过对应的位运算得到存储在 isa 中的其他相关信息。用表示对象引用计数的 extra_rc 来举例,在 arm64 架构下,extra_rc 用 19 bit 来存储。通过对 isa 指针地址进行 >> 45 的位运算就可以得到对象的引用计数值:
以下是 nonPointerIsa 中各个字段存储的信息内容: