八、rw和ro(包含类的成员变量的存储问题的解决)

60 阅读5分钟

本文由快学吧个人写作,以任何形式转载请表明原文出处。

本节为承上启下的一节,上一节中在探索类的属性的时候,发现了rw,下面的内容中还会出现ro,所以本节主要讲述rw和ro的一些内容。

一、官方信息

苹果官方在3年前的2020年的WWDC大会上更新了一段视频,有关类的数据结构发生了变化。相关视频(需要使用mac自带的Safari浏览器观看)。

  1. 当类首次从磁盘加载到内存时的结构 :

图片.png

  1. 当一个类首次被使用时 :

图片.png

  1. 当类在运行时变更了额外的信息时(也就是类被动态修改了,比如例如runtime的API动态的修改类的方法) :

图片.png

可以看到上面是三种时期,类的三种结构变化,从磁盘加载到内存时,只有MYClassclass_ro_t;第一次使用时,有MYClass->class_rw_t->class_ro_t;当类被动态修改时有MYClass->class_rw_t->class_rw_ext_t->class_ro_t

所以我们需要了解的概念就包括了图中的Dirty MemoryClean Memoryclass_rw_tclass_rw_ext_tclass_ro_t

二、DirtyMemory和CleanMemory

两个概念很简单 :

DirtyMemory : 俗称脏内存,表示在进程运行的时候,会更改的内存。

CleanMemory : 俗称净内存,表示加载后便不再会发生更改的内存。

为什么要提出DirtyMemory和CleanMemory的结构和概念,因为类只要一经使用就会变成DirtyMemory,比如在运行的时候会向类中写入数据。为了优化内存的使用,所以提出了这个概念和结构。

之所以说优化了内存的使用,是因为DirtyMemory要比CleanMemory的使用成本高出很多,CleanMemory可以在不用的时候移除,节省很多的内存空间,在使用的时候系统会再次用磁盘中加载;而DirtyMemory则不同,只要进程一直在运行,DirtyMemory就会一直存在。

另外,本节要说的class_ro_t就是CleanMemory,而class_rw_tclass_rw_ext_t则是DirtyMemory。rw被分成了两部分,也是为了尽可能的保持一部分数据的清洁性。通过分离出那些永远不会发生改变的数据,从而优化内存的使用。

三、class_ro_t

1. class_to_t的基本信息

生成时期 : 编译期

ro的意思 : readOnly(只读)

存储的内容 : 类名称、方法、协议、实例变量等信息

ro中是没有分类的方法的,因为分类的方法是在运行时添加的。

源码 : 通过搜索类的本质objc_class :找到如下(逐步进入红框中的源码) :

图片.png

图片.png

只需要看ro的以下常见代码 :

图片.png

2. 解决上一节遗留的成员变量在类中的存储问题

从ro的源码中可以发现一个ivars,这就是上一节遗最后留的问题的答案。成员变量是不存储在rw中的,而是存在于ro的ivars里面。

这里解决上一节的遗留问题,直接用上一节的项目,lldb调试图如下 :

图片.png

到这里之后,看一下class_rw_t的源码,可以找到源码中存在如下函数 :

图片.png

所以class_rw_t的实例可以直接调用ro()函数,获得返回值类型为class_ro_t的数据 :

图片.png

得到的$5的类型是class_ro_t,这就是一个CleanMemory的结构,其内容和上面的class_ro_t的源码中一一对应。

直接打印出ro中的ivars :

图片.png

发现新的类型ivar_list_t,找到源码中的ivar_list_t,发现也是继承于entsize_list_tt,所以也有get()函数,具体原因在上一节中有提到。

那么让ivar_list_t实例($7)直接调用get()函数 :

图片.png

成员变量shenGao终于找到,另外也证明了属性在编译的时候也会变成成员变量。

三、class_rw_t

生成时期 : 运行时

rw的意思 : readWrite(可读可写)

存储的内容 : 类的方法、协议、属性等信息

class_rw_t生成的时间是在运行时,详细的生成时间是在runtime运行realizeClassWithoutSwift函数的时候。

这一步生成了class_rw_t这个结构体,其中包含了上面刚说过的class_ro_t,并且会更新data部分,换成class_rw_t结构体的地址。

realizeClassWithoutSwift的源码 :

图片.png

已经加了注释。

下面就可以放出那两张神图 :

  1. realizeClassWithoutSwift运行之前,data的数据是指向class_ro_t的,也就是常说的ro。

图片.png

  1. realizeClassWithoutSwift运行之后,data的数据是指向class_rw_t的,而class_rw_tclass_ro_t *ro会指向class_ro_t

图片.png

class_rw_t和class_ro_t的异同点总结 :

  1. 共同点 : 都存储了类的属性,协议和方法。
  2. 不同点 :

(1). class_rw_t的生成时期是运行时,而class_ro_t的生成时期是编译期。

(2). class_rw_t生成的时候会将class_ro_t的数据剪切过来,然后将类的分类的属性和方法拷贝到其中。

总结一句 : 可以说class_rw_tclass_ro_t的超集。并且我们实际访问的时候,访问的属性和方法都是class_rw_t中的。

四、class_rw_ext_t

生成条件 :

  1. 通过runtime的API进行动态修改的时候。

  2. 创建或者生成分类的,并且类和分类都是非懒加载的类(也就说,只有在类中调用了+load方法的时候),因为OC的类大多都是懒加载的类,只有在用到的时候才会将类加载到内存中。

苹果在WWDC2020说了class_rw_ext_t是可以减少内存消耗的,大约只有10%左右的类需要动态的修改,所以只有大约10%左右的类会生成class_rw_ext_t

对于动态修改过的类,可以通过class_rw_t中的提供的ext()方法来获取class_rw_ext_t

类会动态更新的成员在上面的图中已经展示过了,下面单独拿出来再看一下。

图片.png

包括了 :方法、属性、协议、DName。

上面解决的上一节的问题,就是一个例子,成员变量在编译时期就确定了,而属性是在运行时还要加载set和get方法,并且会做出一些改变,所以class_rw_t中拥有属性,而class_ro_t中拥有成员变量。