对象的分类
Object- C中所有对象可以分为3类,实例对象,类对象,元类对象。其中我们开发者常用的继承自NSObject都属于实例对象,实例对象通过isa指针指向的是类对象。类对象通过isa指向的是元类对象。类对象和元类对象拥有同样的结构,都是来自objc_class。
实例对象:(isa指针,成员变量的值)
类对象:(isa指针,实例方法、协议、成员变量、属性)
元类对象:(isa指针,类方法)
相信很多开发者都了解的一张图
数据结构
先理解两个名词, "clean memory" 和 "dirty memory" ,字面意思就是 “干净内存” 和 “脏内存”。
clean memory是指加载后不会发生改变的内存。
class_ro_t 就属于clean memory, 因为它是只读的(ro 代表 readonly)dirty memory是指在进程运行时会发生更改的内存,class_rw_t 是 dirty memory(rw 代表 read write)
类结构一经使用就会变成 dirty memory,因为运行时会向它写入新的数据,例如创建一个新的方法缓存并从类中指向它。
dirty memory 比 clean memory 要昂贵得多,只要进程在运行,它就必须一直存在。另一方面 clean memory 可以进行移除,从而节省更多的内存空间。因为如果你需要 clean memory,系统可以从磁盘中重新加载。macOS可以选择换出 dirty memory,但因为 iOS 不使用 swap,所以 dirty memory 在 iOS 中代很大。
dirty memory 是这个类数据被分成两部分的原因,可以保持清洁的数据越多越好,通过分离出那些永远不会更改的数据,可以把大部分的类数据存储为 clean memory。虽然这些数据足以让我们开始,但运行时需要追踪每个类的更多信息,所以当一个类首次被使用,运行时会为它分配额外的存储容量,这个运行时分配的存储容量是 class_rw_t,用于读取-编写数据。在这个数据结构中,我们存储了只有在运行时才会生成的新信息。
拆出了 class_rw_ext_t
所以,我们可以拆掉那些平时不用的部分,这将 class_rw_t 的大小减少了一半。拆出来的部分叫做 “class_rw_ext_t” ,如下图所示:
这个 “class_rw_ext_t” 我们可以在最新的源码 objc4-818.2 中看到:
**
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用。
大约 90% 的类从来不需要这些扩展数据,这在系统范围内可节省大约14MB 的内存,这些内存现在可用于更有效的途径,比如存储你的 app 的数据。
class_rw_t
继续来看class_rw_t的数据结构。
在
rw_t中可以 看到有这3个方法,通过这三个方法分别能获取到类的方法、属性、协议。
rw_ext_t生成条件
- 使用分类的类。
- 使用Runtime API动态修改类的结构的时候。
在遇到以上2种情况的时候,类的结构(属性、协议、方法)发生改变,原有的
ro(Claer Memory,便宜)已经不能继续记录类的属性、协议、方法信息了,于是系统重新生成可读可写的内存结构rw_ext(Dirty Memory, 比较贵),来存放新的类结构。
然后再取方法、属性、列表的时候,在方法实现中来做区分,如果有rw_ext的类,其列表就错那个rw_ext中获得,如果没有,从ro中读取。
思考
元类里面存放的内容只有类方法,那么为什么不把类方法存放在类对象中?或者说设计元类的目的是什么呢?
- 单一职责设计原理。 实例对象存储成员变量的值,类对象存放,实例方法、协议、成员变量、属性,元类对象存放类方法,各司其职,互不影响。
- 复用
msgSend消息发送机制。
类方法、实例方法是在上层的定义,在底层并不区分类方法实例方法,但在runtime这一层,需要承接上层类方法和实例方法,对接到底层方法调用。使用了msgSend,如果msgSend的时候需要再区分类对象,实例对象,会在内部增加判读逻辑,从而降低了效率,有了元类的存在,问题迎刃而解。