iOSRuntime
Runtime概念:
OC是基于C的,区别于C的一点就是OC属于动态语言,并且有面向对象的特性。相比于C,函数的调用在编译的时候会决定调用哪个函数。OC会在编译和链接时做的事情放到了运行时(Runtime)来处理,其调用函数的方法为msg_send,属于动态调用,只有在真正运行才会根据函数名称找到对应的函数来调用。即使调用未实现的方法在编译阶段也不会报错(调用阶段如果不处理仍会crash),而C语言调用未实现的函数在编译阶段就会报错。
runtime常用使用场景:
动态添加类的属性以及方法
方法交换
获取类的属性
字典转模型
等等
数据结构:
//-------- [file:objc-runtime-new.h] --------
struct objc_class : objc_object {
// Class ISA;
Class superclass;
//- 用于快速查找方法执行函数
//- 增量扩展的哈希表结构
//- 局部性原理的应用
cache_t cache; // formerly cache pointer and vtable
//存储类的方法、属性、遵循的协议等信息的地方
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
......
}
对于isa:
指向他的类对象
Root class (class)其实就是 NSObject,NSObject 是没有超类的,所以 Root class(class)的 superclass 指向 nil。
每个 Class 都有一个 isa 指针指向唯一的 Meta class
Root class(meta)的 superclass 指向 Root class(class),也就是 NSObject,形成一个回路。
每个 Meta class 的 isa 指针都指向 Root class (meta)。
为什么要有元类:
因为类方法是存储在元类中的,那么可不可以把元类干掉,在类中把实例方法和类方法存在两个不同的数组中?
答:行是肯定可行的,但是在lookUpImpOrForward执行的时候就得标注上传入的cls到底是实例对象还是类对象,这也就意味着在查找方法的缓存时同样也需要判断cls到底是个啥。
倘若该类存在同名的类方法和实例方法是该调用哪个方法呢?这也就意味着还得给传入的方法带上是类方法还是实例方法的标识,SEL并没有带上当前方法的类型(实例方法还是类方法),参数又多加一个,而我们现在的objc_msgSend()只接收了(id self, SEL _cmd, …)这三种参数,第一个self就是消息的接收者,第二个就是方法,后续的…就是各式各样的参数。
通过元类就可以巧妙的解决上述的问题,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,完美的符合6大设计原则中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。
元类的存在巧妙的简化了实例方法和类方法的调用流程,大大提升了消息发送的效率
对于cache_t:
用散列表来缓存调用过的方法,可以提高访问方法的速度
增量扩展来提高容量
对于cache_t中的bucket_t:
两个成员变量key ,IMP
- key对应selector方法名
- IMP函数地址
对于class_data_bits_t:
class_rw_t* data() {…}
。
其中包括一个指向常量的指针 ro,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议
const class_ro_t *safe_ro() {…}
Objc 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中.
类在内存中的位置是编译期就确定的,类定义的实例方法,会被添加到class_ro_t 的baseMethodList
细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。
整体数据结构图:
Runtime消息机制