isa - 类的底层原理结构

213 阅读11分钟

探究isa的走向

  1. 上篇文章对象的本质和isa探索,之前我们在demo中分析对象p的isa0x011d8001000080f9与上掩码ISA_MASK就得到了我们类OCPeople的地址0x00000001000080f8,如下图: 截屏2021-06-26 下午12.49.34.png 截屏2021-06-22 下午10.19.49.png

  2. 我们用x/4gx 0x00000001000080f8打印输出一下类的地址,也得到了类地址的内存结构,如下图: 截屏2021-06-22 下午10.23.09.png

  3. 我们再用类地址的内存结构中的第一条数据0x00000001000080d0与上掩码ISA_MASK之后po出来看能得到什么,如下图: 截屏2021-06-22 下午10.27.34.png 我们发现po 0x00000001000080d0 & 0x00007ffffffffff8出来的结果竟然是OCPeople

  4. 那我们p/x 0x00000001000080d0 & 0x00007ffffffffff8以16进制打印一下,如下图: 截屏2021-06-22 下午10.29.21.png p/x的结果是0x00000001000080d0。前面第1步中我们得到的地址0x00000001000080f8和现在的地址0x00000001000080d0po出来的结果都是OCPeople。此时我们可以大胆的猜想:当前的类会和我们的对象一样可以无限开辟,也就是在内存里面不止一个类。

  5. 我们来验证一下,展开我们之前写好的OCTestClassNum函数: 截屏2021-06-22 下午10.45.28.png OCTestClassNum函数这样写主要是看NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);输出来的地址是否一致。往下运行到line29,看下NSLog输出结果: 截屏2021-06-22 下午10.40.24.png 我们发现class1,class2,class3,class4打印出来的地址全都是一样的,都是0x1000080f8。意味着0x00000001000080f80x00000001000080d0两个地址中,只有0x00000001000080f8这个地址才是我们的类,0x00000001000080d0不是类,那它是什么? 对象有isa,它指向类,类的isa指向元类,但是我们写的代码中导航栏可以看出是不存在元类的。展开我们当前demo中的Products,这个目录下有个黑色图标的文件,是可执行文件。

  6. 把这个可执行文件用MachOView软件打开,如下图: 截屏2021-06-22 下午11.16.40.png

6.1 展开Section64(__DATA,__objc_classrefs)栏,有个ObjC2 References,点击,可以看到右边出现了_OBJC_CLASS_$_OCPeople,如下图: 截屏2021-06-22 下午11.20.08.png

6.2 点击Section64(__DATA_CONST,__objc_classlist),右边出现的地址还是00000001000080F8,还是OCPeople,如下图: 截屏2021-06-22 下午11.31.55.png

6.3 展开Symbol Table栏,有个*Symbols,点击看到右边出现了很多符号,在右上角搜索class,如下图: 截屏2021-06-22 下午11.48.36.png 能看到有个_OBJC_CLASS_$_OCPeople$是个连接符号,还有个_OBJC_METACLASS_$_OCPeople,在我们编译的可执行文件里面多了个东西,这个类型就是元类METACLASS,但是我们在代码中没有创建,只创建了OCPeople,意味着是系统或者编译器帮我们生成好的。

6.4 元类就是我们当前的类isa所指向的东西,元类是由我们系统生成和编译的。现在我们探索到的isa走向就是:对象 isa --> 类 isa --> 元类,对象的isa指向类,类的isa指向元类。那我们现在思考,元类之后是不是还有一些东西呢?元类的isa指向哪呢?

  1. 我们重新断点运行demo,看下对象的isa与上掩码得到类,类的isa与上掩码得到元类,元类的isa与上掩码的过程,截图如下: 截屏2021-06-24 下午3.48.26.png 元类的isa0x00007fff88a74d60与上掩码得到的还是0x00007fff88a74d60,我们po输出一下得到NSObject。我们发现元类的isa继续走下去,就到了根元类,即元类的isa --> 根元类。

  2. 接下来我们探索NSObjectp/x NSObject.class输出NSObject的地址为0x00007fff88a74d88,发现和第7步得到的地址不一致,再x/4gx 0x00007fff88a74d88输出一下,再拿它的第一条地址与上掩码,得到0x00007fff88a74d60,和第7步最后得到的地址一样,如下图: 截屏2021-06-24 下午4.10.38.png 意味着我们当前NSObject类的isa指向了NSObject的元类,但是NSObject的元类isa指向根元类。而对象p找到根元类经历了三层:对象 isa --> 类 isa --> 元类 isa --> 根元类。NSObject找到根元类只经历了两层,根类 isa --> 根元类isa。

  3. 以上所有探索过程我们就得到了isa走向图:任何对象的isa都可找到当前的类,类的isa就会找向它的元类,元类的isa就会找向它的根元类,根元类又会找向自己。而我们的根类NSObject又会找向NSObject的元类。如图: 截屏2021-06-24 下午5.05.05.png

类的继承链

  1. 这里先展示一个函数OCTestNSObject,关于NSObject的元类链,代码如下: 截屏2021-06-24 下午5.30.25.png 我们断点运行,看下输出结果: 截屏2021-06-24 下午5.37.43.png 我们打印出了NSObject类的实例对象地址0x10052bf80NSObject类的地址0x7fff88a74d88NSObject的元类、根元类、根根元类的地址都是0x7fff88a74d60OCPeople元类的父类是NSObject,地址是0x7fff88a74d60,这个地址和NSObject类的地址不一样,而是和NSObject的元类地址一样。说明OCPeople它的元类的父类是根元类。

  2. 接下来我们创建一个oc文件OCProfessor继承自OCPeople,在函数OCTestNSObject中也添加代码,探究OCProfessor类它的元类的父类是什么,重新运行输出,如下图: 截屏2021-06-24 下午6.03.11.png

截屏2021-06-26 下午12.42.34.png 根据输出打印的结果看到:OCProfessor它的元类的父类指向了OCPeople。所以不是任何类,它的元类的父类都指向根元类。元类它也有一条继承链,任何对象都由类来创造,类指向它的父类,父类指向根类(比如NSObject),根类指向什么还不知道。元类也是类,是类的话就有继承关系,子类的元类继承来自父元类,父类的元类最终会指向NSObject根元类.这个继承链我们也可以画个图看下,如图:

截屏2021-06-26 下午5.53.58.png

  1. 我们来看一下NSObject这一特殊情况,在函数OCTestNSObject中添加代码,查找NSObject的父类:Class superClass4 = class_getSuperclass(NSObject.class);,查找NSObject根元类的父类:Class superClass5 = class_getSuperclass(metaClass);如下图: 截屏2021-06-29 下午1.47.55.png

断点运行,查看输出打印结果,NSObject的父类是(null)没有的,NSObject根元类的父类是NSObject,地址为0x7fff88a91d88,跟前面的类NSObject地址是一样的,又回到了NSObject,说明万物皆来自NSObject,所有的类都来源于NSObject,这也验证了上面“类的继承链”图中,根元类最终指向了NSObject。输出打印如下图: 截屏2021-06-29 下午2.04.39.png 截屏2021-06-26 下午5.53.58 2.png

isa的走向和类的继承

这是一幅来自苹果官方文档上的图(注:苹果官方文档有这张图的网址大家发我一下,我想找那个网址看下,图不是我亲自找的),做了一点标注,如下图: isa流程图的副本.png 大概总结就是:对象的isa指向类,类的isa指向元类,元类的isa指向根元类,根元类的isa指向自己。子类继承来自于父类,父类继承来自于根类,子元类继承来自于父元类,父元类继承来自于根元类,根元类继承指向了NSObject

源码分析类的底层结构

  1. 重新断点运行demo,x/4gx OCPeople.class打印输出,如下图: 截屏2021-06-29 下午3.44.32.png 当前这个OCPeople类有数据结构,有内存。而对象的内存里面会存很多的值,比如isa,成员变量等。那类里面会存什么呢?在探索对象的本质时,我们知道类里面有一个Class isa;,通过typedef struct objc_class *Class;知道Class又是结构体类型指针。

  2. 在objc源码中搜索objc_class,再通过struct objc_class找到其声明的地方,有两处。第一处如图: 截屏2021-06-29 下午4.27.16.png line58有个if判断,非__OBJC2__下才走下面的代码,以及line70的OBJC2_UNAVAILABLE,而我们的代码是OBJC2,所以这处的代码不用看。

  3. 第二处的代码比较长,部分截图如下,我们继续看是否适用: 截屏2021-06-29 下午4.37.35.png 结构体objc_class继承自结构体objc_object

3.1 line1699这行__has_feature判断编译器是否支持,ptrauth_calls身份验证,针对 arm64e 架构,使用Apple A12或者更高版本A系列处理器的设备,比如iPhone XS, iPhone XS Max, and iPhone XR支持 arm64e架构(参考链接1优秀学员链接2)。

3.2 结构体objc_class内部存储了一些成员变量。 截屏2021-06-29 下午10.41.15.png

  • ISA隐藏属性,它来自于objc_object,继承过来的,可以在源码里搜索objc_objectstruct objc_object里面就有成员变量Class _Nonnull isa。还有superclasscachebits这些成员变量。
  • 在研究对象的本质时,我们用到clang命令将main.m文件转化为main.cpp文件,在main.cpp文件中我们看到了method_listivar_listprop_listprotocol_list,这些都存在于bits里面。

3.3 源码继续往下看,这里是获取父类getSuperclass、设置父类setSuperclass截屏2021-06-29 下午10.44.48.png

3.4 class_rw_t是直接从bits.data()获取来的,command点进去class_rw_t,它是struct结构体类型的,里面有很多的方法: 截屏2021-06-29 下午10.51.53.png 现在我们已知类objc_class,要想办法获取bits,获取内存数据data,首先要了解下内存偏移。

内存偏移

  1. 创建一个demo2或者在之前的demo上修改也一样,代码如下: 截屏2021-06-29 下午11.37.24.png

输出结果如下图,ab的值都是输出10,但是a的地址输出为0x7ffeefbff3acb的地址输出为0x7ffeefbff3a8截屏2021-06-29 下午11.37.45.png

可以理解为某个区域有两个指针,这两个指针同时指向数据10,这个数据可以被任何指针指用访问。相当于copy的值拷贝。画图表示如下图: 截屏2021-06-30 下午2.54.27.png 2. 在demo2上添加代码如下: 截屏2021-06-30 下午2.25.47.png

看下对象p1p2输出结果: 截屏2021-06-30 下午2.27.35.png p1p2输出地址不一样,&p1&p2输出地址也不一样。&p1&p2都是指针的指针,也叫二级指针。 截屏2021-06-30 下午2.54.36.png 3. 我们看下数组指针,代码如下,定义一个数组cc里面存放了1、2、3、4,然后把c赋给指针d截屏2021-06-30 下午3.05.43.png

看下输出结果,&c&c[0]&c[1]的输出结果依次为0x7ffeefbff3c00x7ffeefbff3c00x7ffeefbff3c4dd+1d+2的输出结果依次为0x7ffeefbff3c00x7ffeefbff3c40x7ffeefbff3c8。数组c的地址也就是第一个元素的地址。 截屏2021-06-30 下午3.09.53.png

在数组c的内存中,第一个元素从c的首地址处开始存,内存连续开始依次存放第二个元素、第三个元素、第四个元素。c[0]c[1]的地址相差4字节 ,刚好是元素类型int的大小。d是数组c的地址指针,d+1是指针往下平移一个,平移一个步长。画一下大概的内存分布如下: 截屏2021-06-30 下午3.45.04.png

我们平时输出数组c的元素,最常用的就是下面的写法: 截屏2021-06-30 下午3.49.45.png 也可以用下面的写法: 截屏2021-06-30 下午3.49.54.png 内存可以平移,平移之后取地址里面的值,就能得到内存里面的值。

获取内存数据

获取类的地址之后,类的地址就是内存结构的首地址,内存平移一些大小,就能获取内存里面的内容。

  1. 在objc源码中断点运行,如下图: 截屏2021-07-01 上午10.02.35.png

x/4gx OCPeople.class来查看类OCPeople的数据结构,打印如下: 截屏2021-07-01 下午4.54.45.png 在上面源码分析类的底层结构时,我们知道了类的底层结构。第一个isa0x00000001000086b8就是这个类的isa

第二个superclass,那0x000000010036a140OCPeople的父类吗?我们可以用po 0x000000010036a140验证一下,输出了NSObject,很完美。我们当前的OCPeople就是继承自NSObject,如下图。也可以用p/x NSObject.class 16进制打印一下NSObject的地址,打印出的地址也是0x000000010036a140截屏2021-07-01 下午4.48.54.png

第三个cache,这个0x000000010123aa80后面的博客也会分析到,今天先不在这里提。

第四个bits,就是这个0x0001803000000003bits里面有datadata里有methodlist等一些东西。从OCPeople类的首地址平移Class ISAClass superclasscache_t cache的长度大小才能得到bits的地址。isa的大小是8没问题,superclass是8字节,cache大小呢?