iOS对象的底层探索(下)

上篇文章已经验证了结构体中成员变量顺序不同,对于内存分配上有影响的,那类中是否也有同样的影响呢? 我们来验证一下,首先我们创建一个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按浮点数格式显示)

t1.png 从图中我们可以看出第二个内存地址存储来三个值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类型,也就是联合体。那我们先来了解一下什么是联合体。 t2.png 我们发现person1这个结构体可以获取三次赋值的所以值,而person2这个联合体每次赋值后其他的值都会被同步修改,这是因为联合体共用一块内存空间,打印他们的内存地址都是相同的 t3.png 共用的内存空间为可以容纳最大的成员变量,且是成员变量最大类型(基本数据类型)的整数倍。
结构体和联合体的区别:结构体中成员变量可以共存,联合体成员变量互斥,节省一定的内存空间。
了解了联合体后我们继续探索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这种形式,这种方式是位域的结构,那我们来简单了解一下位域。 t4.png 正常来说,一个这样的结构体需要占用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位即可。 t6.png 到这里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指针获取内存地址。多写写属性,最好不要写成员变量,毕竟属性系统会帮你优化内存。

猜你喜欢

转载自juejin.im/post/7088248432879468575