本文主要内容
1.探索ivar的存储位置
2.ro、rw、rwe解析
3.类方法的存储位置
4.关于元类的解释
5.通过runtime的API探索类的数据结构
一、探索ivar的存储位置
上一篇研究了类的部分底层
,了解到isa的走位图以及类对象和元类对象的继承关系,并且知道类的本质是一个叫作objc_class的结构体
,其中包含了ISA
指针、(类对象的)父类、cache
、bits
等,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
结构体中。
我们来验证类的成员变量是否真的存储在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个成员变量)如下图:
知识小亮点
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修改
,其中分类和本类必须是非懒加载类
。
分析源码,会判断rw
是否存在rwe
,如果存在rwe
,就会在其中找需要修改的内容(方法、协议等),如果不存在rwe
就会去ro
中找。
三、类方法的存储位置
2个疑问
的另一个疑问是:类方法存在在哪里?
猜测:上一篇文章中发现,类方法并不在类的bits
数据中,那类方法是否会在类的元类的bits
中呢?我们带着疑问进行探索。首先通过类的isa指针指向
找到元类的内存地址,再使用获取bits中方法列表的方法
找到元类中的方法列表即可(详细分析过程此处省略,如有不清楚的地方,请查看上一篇文章),分析如下图:
知识小亮点
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等内容。
通过runtime中的
class_copyIvarList
函数拿到成员变量列表ivars
,遍历即可获取所有的成员变量。
注意⚠️:
class_copyIvarList
方法返回的ivar *
类型,该方法中使用malloc 开辟了内存空间,而ARC进行内存管理只会管理OC对象,所以需要用free
释放ivars
。
2.获取类的属性
通过runtime中的class_copyPropertyList
函数拿到属性列表properties
,遍历即可获取类的所有属性name、age、height。具体实现如下图:
打印属性类型解析:
显示如属性name“T@'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
2.获取实例方法和类方法
(1)实例方法
通过runtime中的class_copyMethodList
函数拿到方法列表methods
,遍历即可获取实例对象的所有方法。具体实现如下图:
打印属性类型解析:
显示如方法name类型“@16@0:8”.其中"@"代表返回值为对象类型,"16"代表方法参数的长度,第2个"@"代表方法的接收者,8个长度,从0-7;":"代表方法名,8个长度,从8-15,所以总共16个长度.
再如属性setHeight“v24@0:8q16”,"v"代表返回值为void类型,"24"代表方法参数的长度,第2个"@"代表方法的接收者,8个长度,从0-7;":"代表方法名,8个长度,从8-15;还有个long类型的参数height,8个长度,从16-23,所以总共24个长度.
(2)类方法
通过runtime中的class_getInstanceMethod
函数传入元类也能得到类方法的内存地址。为什么?根据我们了解,获取类方法可以使用class_getClassMethod
函数,所以objc底层并没有实例方法和类方法之分
。
查看源码发现:
class_getClassMethod
实际上调用class_getInstance Method
函数。
由此进一步说明,苹果设计元类的目的并不是存放类方法而是为了复用消息机制!!!
3.获取方法的实现imp
通过runtime中的class_getMethodImplementation
函数获取方法的实现。观察发现,此函数既能通过类找到实例方法的实现,也能通过元类找到。为什么呢?
查看
class_getMethodImplementation
函数的源码发现,如果找不到方法的实现,会返回_objc_msgForward
来进行消息转发
。
本文总结
1.类的ro中存储成员变量,实例方法、属性、协议等也存储在类对象中;
2.类方法存储在元类中;
3.元类的设计是为了复用消息机制,并非为了存放类方法;
4.objc底层并没有实例方法和类方法之分。
有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍