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

语言: CN / TW / HK

本文主要内容

1.探索ivar的存储位置
2.ro、rw、rwe解析
3.类方法的存储位置
4.关于元类的解释
5.通过runtime的API探索类的数据结构

一、探索ivar的存储位置

上一篇研究了类的部分底层,了解到isa的走位图以及类对象和元类对象的继承关系,并且知道类的本质是一个叫作objc_class的结构体,其中包含了ISA指针、(类对象的)父类、cachebits等,bits中存储了类的属性实例方法协议等内容,而这些内容都放在一个叫class_rw_t的结构体中。同时还留下2个疑问,第1个疑问是:成员变量并没有在bits中属性列表中展示。现在我们来研究类的成员变量到底存储在什么位置? 通过分析并查看objc4-838.1源码发现,class_rw_t结构体中有1个叫作ro的结构体,返回class_ro_t结构体类型的数据,而class_ro_t结构体中包含ivars,即代表成员变量,也就是说类的成员变量就存储在class_ro_t结构体中。

image.png

image.png

我们来验证类的成员变量是否真的存储在class_ro_t结构体中。方法同上一篇获取bits里面的methodlists和propertylist类似。读取bits的返回class_data_bits_t数据内容,通过data函数读取得到class_rw_t结构体类型数据,通过*取值,再调用ro函数获取到class_ro_t结构体类型数据,其中ivars就是成员变量!调用ivars即可返回ovar_list_t类型数据,此时同前面属性和方法的获取一样了。通过get函数获取其中每个ivar类型元素的数据,从而找到累的成员变量(包含5个成员变量)如下图:

image.png

image.png

知识小亮点
A:为何真正的成员变量放在类中,而成员变量的值却放在实例对象中(前面已经知道,实例对象包含isa指针和成员变量的值)? 
Q:类的本质是一个结构体,这个结构体相当于一个模版,这个模版中有成员变量、属性、方法、协议等内存,实例对象就是根据类的模版生成的,在创建实例对象时,不同的实例对象成员变量的值是不一样的,所以就把不一样的成员变量的值存放在不同的实例对象中。

二.ro、rw、rwe解析

ro是在编译时生成,当类进行编译时,类的属性实例方法协议等就会存在于ro结构体中,它是一块纯净的、只读(read only)的内存空间,不允许被修改,即clean memory。rw是在运行时生成的,类一经使用ro就会变成rw,即rw会把ro中的内容"剪切"到rw中,可读可写(read&write)。而runtime提供了动态为类添加方法和属性的API,这些方法和属性存在于只读不允许修改的ro中,如果想要修改方法和属性(一般修改比例为10%左右),需要把ro中的内容"拷贝"到rw中,但这样就会存在两份ro,增加内存消耗。所以苹果通过class_rw_ext_t(rwe)结构体来解决这10%左右的修改,这些修改主要指分类或者runtime API修改,其中分类和本类必须是非懒加载类

image.png

image.png

分析源码,会判断rw是否存在rwe,如果存在rwe,就会在其中找需要修改的内容(方法、协议等),如果不存在rwe就会去ro中找。

image.png

三、类方法的存储位置

2个疑问的另一个疑问是:类方法存在在哪里?
猜测:上一篇文章中发现,类方法并不在类的bits数据中,那类方法是否会在类的元类的bits中呢?我们带着疑问进行探索。首先通过类的isa指针指向找到元类的内存地址,再使用获取bits中方法列表的方法找到元类中的方法列表即可(详细分析过程此处省略,如有不清楚的地方,请查看上一篇文章),分析如下图:

image.png

知识小亮点
ro存在磁盘中,使用时内存加载。只要APP运行rw就会一直存在,APP杀死才会释放。

结论类方法确实存储在元类中!

四、关于元类的解释

上一篇文章第二部分还有一个疑问:什么是元类?为什么要引出元类呢?也就是说苹果为什么要设计这个元类呢?
这是为了**复用消息机制**,用同一套消息机制。在OC中调用方法,如[HGPerson alloc],在苹果系统中实际上就是给HGPerson发送某条消息,调用方法编译时就会编译为包含2个参数的函数objc_msgSend(消息接收者 isa,消息方法名),通过这个函数根据消息接收者的isa指针找到该方法的实现,如消息接收者是实例对象,就会到实例对象isa指针指向的类对象中找该方法的实现,如果消息接收者是类对象,就会到类对象isa指针指向的元类对象中找该方法的实现(所以类方法和实例方法可同名)。
如果没有元类,只用2个参数无法找到方法的实现,需要修改为:objc_msgSend(消息接收者,消息方法名,判断实例对象/类对象,判断实例方法/类方法),而消息的发送最重要的是快速,如果添加上述这些判断结构会影响发送效率!利用当前的消息机制只通过isa指针可以很快找到方法的实现,实例对象存储成员变量的值,类对象存储实例对象的方法,元类对象存储类对象的方法,也就是单一职责的原则,大大增加消息发送的效率,同时维护同一个消息机制(objc_msgSend函数)也更方便。

五、通过runtime的API探索类的数据结构

1.获取类的成员变量
成员变量为ivar_t类型的结构体,其中包含name、type、size等内容。

image.png 通过runtime中的class_copyIvarList函数拿到成员变量列表ivars,遍历即可获取所有的成员变量。

image.png

注意⚠️:class_copyIvarList方法返回的ivar *类型,该方法中使用malloc 开辟了内存空间,而ARC进行内存管理只会管理OC对象,所以需要用free释放ivarsimage.png

2.获取类的属性
通过runtime中的class_copyPropertyList函数拿到属性列表properties,遍历即可获取类的所有属性name、age、height。具体实现如下图:

image.png

打印属性类型解析:
显示如属性name“[email protected]'NSString',C,N,V_name”。其中"T"代表类型,后面加"@‘NSString’即为字符串类型,"C"代表copy,"N"代表"nonatomic","V_name"代表成员变量_name.
再如属性age”Ti,N,V_age**“,"Ti"整体代表int类型,"N"代表"nonatomic","V_age"代表成员变量_age.

属性类型编码说明官网地址:Declared property type encodings

image.png image.png

2.获取实例方法和类方法
(1)实例方法 通过runtime中的class_copyMethodList函数拿到方法列表methods,遍历即可获取实例对象的所有方法。具体实现如下图:

image.png

打印属性类型解析:
显示如方法name类型“@[email protected]:8”.其中"@"代表返回值为对象类型,"16"代表方法参数的长度,第2个"@"代表方法的接收者,8个长度,从0-7;":"代表方法名,8个长度,从8-15,所以总共16个长度.
再如属性setHeight“[email protected]:8q16”,"v"代表返回值为void类型,"24"代表方法参数的长度,第2个"@"代表方法的接收者,8个长度,从0-7;":"代表方法名,8个长度,从8-15;还有个long类型的参数height,8个长度,从16-23,所以总共24个长度.

image.png

(2)类方法
通过runtime中的class_getInstanceMethod函数传入元类也能得到类方法的内存地址。为什么?根据我们了解,获取类方法可以使用class_getClassMethod函数,所以objc底层并没有实例方法和类方法之分

image.png 查看源码发现:class_getClassMethod实际上调用class_getInstance Method函数。 由此进一步说明,苹果设计元类的目的并不是存放类方法而是为了复用消息机制!!!

image.png

3.获取方法的实现imp
通过runtime中的class_getMethodImplementation函数获取方法的实现。观察发现,此函数既能通过类找到实例方法的实现,也能通过元类找到。为什么呢?

image.png 查看class_getMethodImplementation函数的源码发现,如果找不到方法的实现,会返回_objc_msgForward来进行消息转发

image.png

本文总结

1.类的ro中存储成员变量,实例方法、属性、协议等也存储在类对象中;
2.类方法存储在元类中;
3.元类的设计是为了复用消息机制,并非为了存放类方法;
4.objc底层并没有实例方法和类方法之分。

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