上篇文章已经验证了结构体中成员变量顺序不同,对于内存分配上有影响的,那类中是否也有同样的影响呢? 我们来验证一下,首先我们创建一个WTPerson的类,并且实例化一个对象然后对其进行赋值,如果变量顺序同样影响内存分配的话,该对象的内存应该如图
@interface WTPerson : NSObject //(错误的分配方式)
@property (nonatomic, copy) NSString *name; // 8字节 从8开始 15结束
@property (nonatomic, assign) short age; // 2字节 从16开始 17结束
@property (nonatomic, assign) int isFree; // 4字节 18不能整除4 20开始 23结束
@property (nonatomic, copy) NSString *nickName; // 8字节 24开始 31结束
@property (nonatomic, assign) short sex; // 2字节 32开始 33结束
+ (void)testMethod;
- (void)testMethod;
@end
WTPerson *p = [[WTPerson alloc] init];
p.name = @"Vitus";
p.age = 67;
p.sex = 1;
NSLog(@"对象至少需要的内存大小--%lu",class_getInstanceSize([p class])); //33
NSLog(@"系统分配的内存大小--%lu",malloc_size((__bridge const void *)(p))); //48
复制代码
如果变量顺序真的影响内存分配的话,应该打印需要内存33,分配内存48,可是实际上打印的结果如下
2022-04-19 10:45:22.985645+0800 KCObjcBuild[30642:791877] 对象至少需要的内存大小--32
2022-04-19 10:45:22.986840+0800 KCObjcBuild[30642:791877] 系统分配的内存大小--32
复制代码
可以看出对象中变量的顺序没有对内存空间造成影响,而且也验证了方法其实不在分配的内存空间中,那这个属性的值是怎么存储的呢,增加多一些属性来使用llvm打印一下,这里我们需要用的常用的p和po指令,以及x/nuf
指令。\
我们先来了解一下x/nuf:
x:代表16进制打印
n:代表打印n个地址单元
u:代表一个地址单元长度(g代表8字节 b代表单字节 h代表双字节 w代表4字节)
f:代表显示变量的方式(x按16进制显示 d按10进制显示 u按10进制格式显示无符号整形 o按8进制显示 t按2进制显示 c按字符格式显示 f按浮点数格式显示)
从图中我们可以看出第二个内存地址存储来三个值age的0x00000043,sex的0x01,isFree的0x01。这说明实际上系统在对象层面已经对内存对齐进行了优化,不管你属性是怎么样排列的,将数据存储到内存中时都会进行优化处理。
那所有的对象都会进行内存优化吗?我们将属性改为成员变量,再来看一下
@interface WTPerson : NSObject {
@public
short sex;
NSString *name;
NSString *nickName;
int age;
}
@end
2022-04-19 13:24:44.273136+0800 KCObjcBuild[35610:954602] 对象至少需要的内存大小--40
2022-04-19 13:24:44.274207+0800 KCObjcBuild[35610:954602] 系统分配的内存大小--48
复制代码
这时我们会发现,内存没有被优化成32,所以系统只是对于属性生成的成员变量是进行的优化,而不会对自己定义的成员变量进行优化。不过这种内存消耗真的很小,最多16个字节,1024字节才1kb,真的可以忽略不计,而且现在都是用属性来生成的成员变量,自己写成员变量真的很少了。
iSA
已经知道了影响内存的因素都有哪些,那我们再来聊一聊isa指针,我们知道所以的object都有一个8字节的isa指针,那isa指针到底是什么?到底有什么用? 在上一篇文章中我们知道创建对象的方法_class_createInstanceFromZone,在里面使用obj->initIsa(cls); 来初始化isa指针,在objc-object.h中找到相应方法,我们发现isa的类型是 isa_t
newisa(0);在objc-private.h文件中发现其是union类型,也就是联合体。那我们先来了解一下什么是联合体。 我们发现person1这个结构体可以获取三次赋值的所以值,而person2这个联合体每次赋值后其他的值都会被同步修改,这是因为联合体共用一块内存空间,打印他们的内存地址都是相同的 共用的内存空间为可以容纳最大的成员变量,且是成员变量最大类型(基本数据类型)的整数倍。
结构体和联合体的区别:结构体中成员变量可以共存,联合体成员变量互斥,节省一定的内存空间。
了解了联合体后我们继续探索isa_t,其中struct { ISA_BITFIELD; // defined in isa.h }
; 这个代表isa_t的属性,我们查看一下ISA_BITFIELD都有什么。
# if __arm64__
......其他系统
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD
uintptr_t nonpointer : 1; //标识是否为nonpointer
uintptr_t has_assoc : 1; //是否有关联对象
uintptr_t has_cxx_dtor : 1; //该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
uintptr_t shiftcls : 44; // 存储对象的指针
uintptr_t magic : 6; //⽤于调试器判断当前对象是真的对象还是没有初始化的空间
uintptr_t weakly_referenced : 1; //指对象是否被指向或者曾经指向⼀个ARC的弱变量,没有弱引⽤的对象可以更快释放
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1; //当对象引用计数大于extra_rc所能存储的最大范围时,需借用该变量
uintptr_t extra_rc : 8 //当表示该对象的引⽤计数值,实际上是引⽤计数值减 1
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# endif
复制代码
我们可以看到ISA_BITFIELD
内部的成员都是使用的: 1这种形式,这种方式是位域的结构,那我们来简单了解一下位域。 正常来说,一个这样的结构体需要占用3个字节来表示所存储的数据,但是当使用了位域了以后,我们只需要2个字节就能够把内容给存储下来(a占第一个字节的3位,b需要6个bit,放在第一个字节中会超出字节,所以开辟第二个字节放入6位,c放在第二个字节的第7位),因此位域的作用,也是为了让内存更加优化。、 现在isa的数据结构已经了解了,那接下来看看initIsa的赋值操作
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
...... 对位域的一系列赋值 ......
newisa.setClass(cls, this);
}
isa = newisa;
复制代码
我们可以看到,当nonpointer为0的时候,直接绑定类和地址的对应关系,而当nonpointer为1的时候,除了保存类的信息以外,还会保存一些额外的特殊信息,我们称之为NonpointerIsa。isa其实并不单单是一个指针,以x86_64架构为例,实际上有44位用于存储对象地址。其余位用来存储一些特殊的值。
既然isa中不只有类信息,其中还存在别的特殊信息,那我们怎么屏蔽其他的特殊信息,直接找到类信息呢?
第一种方式就是使用源码种提供的ISA_MASK
进行掩码 0x011d800100008a61 & 0x00007ffffffffff8ULL就可以获取到类名
第二种方式是对isa指针进行位运算,我们知道对象地址存储在44位上,前面3位和后面17位存储的是特殊的值,那我们只需要清除前面3位和后面17位的值,就能拿到我们需要的类名。
即0x011d800100008a61 >> 3 << 20 >> 17,同样可以获取类名,同理也可以通过位运算获取该对象的引用计数,右移56位即可。 到这里isa的探索暂告段落,isa最主要的作用是记录对象指针地址,指针地址用不到64位这么大的内存,所以同步记录对象的相应状态来优化内存和优化对对象的操作判断。
initIsa后在_class_createInstanceFromZone中已完成对象的创建和指针地址绑定,alloc流程就已经完成,我们就获取到了一个对象,我们也知道init中只是return self;方便我们进行扩展initWith..,那创建对象常写的第三个关键字new到底是如何创建的对象呢?
在NSObject.m中我们查找到+ (id)new { return [callAlloc(self, false/checkNil/) init]; },我们知道alloc调用的也是callAlloc
方法,那我们就可以认为 [ class new] 其实等价于 [[class alloc] init]。
总结
对象的创建是alloc进行的创建,其中分配内存地址都是按照16字节的整数倍进行的分配,优先开辟对象的内存空间,然后在初始化isa指针时将内存空间和对象进行绑定,这一过程中我们也了解到了对象的属性值怎么存储的,结构体的对齐,联合体和位域对于内存空间的优化,isa指针都存储了什么,怎么用isa指针获取内存地址。多写写属性,最好不要写成员变量,毕竟属性系统会帮你优化内存。