iOS底层原理之对象的底层探索(下)

2,212 阅读9分钟

本文主要内容

1.影响对象内存的因素
2.对象的内存分布
3.位域和联合体
4.nonPointerisa
5.如何利用isa的位运算得到类对象
6.new方法

一、研究影响对象内存的因素

创建工程"01_影响对象内存的因素",新建HGPerson类,给类添加一些属性和方法(有实现),如下图

image.png

main.m中创建类对象,并给其属性进行赋值,通过手动计算并且打印对象需要的内存大小(isa + 成员变量的值)和系统实际分配的内存大小分别为40字节、48字节,发现和上一篇中结论一致:对象内部需要内存为8字节对齐,系统实际分配的内存为16字节对齐!

image.png

说明:
对象需要的内存大小计算:isa + 成员变量的值=8+8+8+4+8+2=40(388字节对齐)
系统实际分配的内存大小计算:16字节对齐,所以为48。

同时还得出一个结论:系统实际分配给对象的内存大小不仅和类对象中添加的方法无关,同时也与成员变量(属性)的顺序无关

所以影响对象内存的因素只有对象的成员变量的值,与其他因素无关!

补充内容影响对象内存因素的特殊情况

如下图,自定义父类和子类,父类中属性count在1号位置和2号位置时,系统对对象的成员变量顺序不做重排!因此在1号位置时对象需要内存和系统分配的内存分别为72和80,2号位置时对象需要内存和系统分配的内存均为64

image.png

image.png

出现以上问题的原因:当子类继承父类的数据结构时,父类是一块连续的内存空间,子类无法修改父类的数据结构,因此此例中"HGHuman"中count的位置不同对子类的对象的内存大小产生了影响。
⚠️注意:在本例中替换不同的位置,内存大小变化为16个字节(系统分配),对于内存的消耗相对很小,在开发过程中几乎可以忽略!

二、对象的内存分布

那么系统分配给对象的内存中,数据是如何排列的?其中的属性的内存分布会按照声明或者赋值时的顺序排列吗?如下图,通过LLDB调试,x/6gx p打印对象p的内存(x/6gx详见汇编指令和LLDB常用命令汇总中的x/nuf说明)。

image.png

注意:观察内存分布,发现属性age、number所占内存共6字节,会被系统放到同一个8字节中,达到内存优化的目的!

由上图调试得出结论:对象的内存分布与属性声明或赋值顺序无关,苹果系统会自动重排对象的属性的顺序,达到内存优化的目的!

三、位域和联合体

1.位域
写法如下图中,":"后面表示"位数",1个字节8位,其范围由前面数据类型决定。如果数据类型为int,则甚至可以写成"32"。

image.png

注意:

  • 后面位域的长度不能超过数据类型最大长度。如图中"a:"后面的数字不能超过7
  • 1个位域必须存储在一个位域中,超过则存放到下一个字节。如图中“a:”后面为7,则b必须存储到下一字节,所以图中struct3的大小为4个字节。特别注意:"struct4"的大小还需遵循"结构体对齐规则"(见iOS底层原理之对象的底层探索(上))
  • 此方法用于节省内存,但由于节省内存有限,只有在系统级别才会使用,日常开发中很少使用。

2.联合体
联合体写法类似结构体,只是把"struct"换成"union"。通过打印结构体和联 会开辟足够的内存给结构体,结构体会依次给对应成员变量赋值,可同时有值。而系统给联合体开辟足够打的内存,赋值时,其中成员变量互斥,只能给某一个成员变量赋值。如下图:

image.png

打印联合体内成员变量的内存发现是同一块内存image.png

联合体内存大小计算规则

1.必须能够容纳最大的成员变量;
2.通过"1."计算出来的大小必须是其最大成员变量(基本数据类型)的整数倍。

结构体HGTeacher2内存大小为8,联合体HGTeacher3内存大小为8(a为7,int为4,根据规则为8),联合体HGTeacher4内存大小为5(基本数据类型char是1字节)。

image.png

总结
结构体和联合体的区别

  • 两者内存大小的计算规则不同;
  • 结构体成员变量可以共存,内存开辟粗放;
  • 联合体成员变量互斥,可以一定的节省内存空间。

四、引出isa和nonPointerisa

回顾iOS底层原理之对象的底层探索(上),跟踪objc源码到“计算对象所需内存的大小instanceSize函数和系统实际分配给对象的内存大小calloc(1, size)的位置。经过分析,由下图可知,此时(断点1之前)分配给对象的内存并未和类绑定,而执行initInstanceIsa函数(或initIsa函数),即断点2后,通过LLDB调试发现,对象obj已经和LGPerson类绑定。因此需要研究initInstanceIsa函数(或initIsa函数)内部即函数objc_object::initIsa的工作。

image.png

进入函数objc_object::initIsa发现,创建了一个isa_t的isa,isa_t是一个联合体。
此时引出nonPointerIsa,内存优化的手段。Isa指针是一个class类型的结构体指针,主要用来存储内存地址。Isa指针有8个字节即64位,存储类对象的内存地址无需64位空间,每个对象都存在Isa指针会导致内存的浪费,为了减少浪费,系统将与对象息息相关的信息存在此空间(64位),如下图中的引用计数extra_rc,此时就用到nonPointerIsa。【此处突然引出时有疑问突然就引出nonPointerIsa,没反应过来,其实就是分析了Isa指针后需要进行内存优化,所以就引出!

image.png

注意:    
1.isTaggedPointer也是内存优化手段。
2.”newisa.extra_rc = 1;“意为:对象引用计数的值,即通过alloc创建出来的对象引用计数即为1.

前面说到isa_t是一个联合体。以前版本均为Class类型的结构体指针,而当前普遍使用nonPointerIsa,为了兼容性所以isa_t采用联合体类型。

image.png

五、如何利用isa的位运算得到类对象

基于的当前都是nonPointerIsa,isa指针存储的内容保存到了名为ISA_BITFIELD(上图中)的结构体中。跳转到此结构体中,由于本人使用的intel64位处理器,所以应用如下图中的结构体内容:

image.png

ISA_BITFIELD结构体解析

  • 低位存储nonpointerhas_assochas_cxx_dtor
  • shiftcls
  • 高位存储magicweakly_referencedunusedhas_sidetable_rcextra_rc.

每一“位”具体代表说明如下:
nonPotinter_isa

1.nonpointer:表示是否对isa指针开启指针优化,0为纯isa指针,1为不止是类对象地址,isa指针中包含了类信息、对象的引用计数等;
2.has_assoc:关联对象标志位,0没有,1存在;
3.has_cxx_dtor:该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象;
4.shiftcls:存储类指针的值,开启指针优化的情况下,在intel 64架构中由44位用来存储类指针;
5.magic:用于调试器判断当前对象是真的对象还是没有初始化的空间;
6.weakly_referenced:标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放;
7.unused:标志未被使用的空间【自己理解】
8.has_sidetable_rc:当对象引用计数大于extra_rc所能存储的最大范围时,则需要借用该变量存储进位;
9.extra_rc:表示该对象的引用计数值,当对象的引用计数超出最大的存储范围时,则需要使用到上面的has_sidetable_rc。

分析HGPerson的实例对象p,使用如下方法,获取并验证类对象的内存地址:
在intel 64架构(x86_64)中,将isa指针内存地址右移3位(清除低3位),左移20位(清除高17位),再右移17位,得到的内存地址即为类对象的内存地址。实操如下图:
模拟器调试: image.png 真机调试:

image.png

补充内容:

1.验证引用计数的值
通过内存地址的变换,也可获取到对象引用计数的值,即最高1位的值(真机):将isa指针地址右移45位(1+1+1+33+6+1+1+1),得到引用计数为2;

image.png

注意⚠️:模拟器验证有问题,待解决!!!

通过探究发现使用isa & ISA_MASK(对应架构)即可得到类对象的内存地址!

模拟器: image.png

真机: image.png

2.无法打断点的问题解决 在调试过程中发现,在某处打了断点后,运行工程,在打了断点的位置不会停止,并且断点位置显示为虚线的。出现这个问题可能是因为系统优化导致的,解决方法:找到"Build Setting" 搜索lto,设置“Link-Time Optimization”为“No”一般都可解决。如果不能解决,可能是其他原因导致的,因情况而定。

image.png

六、new方法

探究new方法的实质,首先创建工程"06_new方法",在类HGPerson,重写init方法,如下图。使用alloc-init和new分别创建对象p、p1,打印两者属性发现均已被赋值。

image.png

image.png

知识扩展:工厂设计模式
使用对工厂的继承从而创造出其他的子工厂,在其子工厂进行创建类的对象,后续直接调用该工厂的那个重写方法即可。如是说,NSObject中有init方法,即为工厂模式,其子类在创建时,即可用此方法进行属性等的初始化,得出不同的对象,但父类NSObject本身init没有做事情。

跟踪汇编及源码
new的方法源码如下:

image.png

new方法实际调用了callAllocinit方法,callAlloc实际又调用了alloc,所以可看做new[[alloc] init]意义相同。跟踪汇编发现,先调用了objc_opt_new函数,找到对应源码同new方法同理:

image.png

image.png

本文总结

  1. 探究对象内存分布
  2. 探究isa指针本质
  3. 了解new方法

有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍