一、Runtime概念
RunTime简称运行时,其中最主要的是消息机制。
对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。
OC的函数调用成为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错)。
-
只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
其动态性体现在三个方面:
1.动态类型:
即运行时再决定对象的类型。简单说就是id类型,任何对象都可以被id指针所指,只有在运行时才能决定是什么类型。像内置的明确的基本类型都属于静态类型(int、NSString等)。静态类型在编译的时候就能被识别出来。所以,若程序发生了类型不对应,编译器就会发出警告。而动态类型就编译器编译的时候是不能被识别的,要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。
2.动态绑定:
基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。比如我们一般向一个NSObject对象发送-respondsToSelector:或者 -instancesRespondToSelector:等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类 的+resolveClassMethod:和+resolveInstanceMethod:将会被调用,在此时有机会动态地向类或者实例添加新的方 法,也即类的实现是可以动态绑定的;isKindOfClass也是一样的道理。
3.动态加载:
所谓动态加载就是我们做开发的时候icon图片的时候在Retina设备上要多添加一个张@2x的图片,当设备更换的时候,图片也会自动的替换。
二、Runtime数据结构
1.Class
Objective-C类是由Class类型来表示,Class 其实是指向 objc_class 结构体的指针
typedef struct objc_class *Class;
我们可以从<objc/runtime.h>里面看到类的定义
struct object_class{
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list *methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
}OBJC2_UNAVAILABLE;
2.id
id 是一个结构体指针类型,它可以指向 Objective-C 中的任何对象。从<objc/objc.h>中可以看出id其实是指向objc_object结构体的指针。objc_object只有一个成员变量 isa,对象可以通过 isa 指针找到其所属的类,这也是为什么id可以表示任意对象的原因。isa 是一个 Class 类型的成员变量。
注意:在KVO中,isa在运行时会被修改,指向一个中间类,对于编译器而言,isa的指向才是最真实的类型。
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
当我们向一个Objective-C对象发送消息时,运行时库会根据
实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表由super_class指针找到父类的方法列表直至根类NSObject中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。
3.元类(meta-Class)
类自身也是一种对象,可以叫做“类对象”。类对象包含一个指向其类的一个isa指针( Class _Nonnull isa )
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,meta-class中存储着一个类的所有类方法。
所以,调用类方法的这个类对象的isa指针指向的就是meta-class。当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
下图是一个经典的类及相应meta-class类的一个继承体系图解:
从图中可以看出:
1.每个实例对象的类都是类对象,每个类对象的类都是元类对象,每个元类对象的类都是根元类(root meta class的isa指向自身)。即任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
2.类对象的父类最终继承自根类对象NSObject,NSObject的父类为nil
3.元类对象(包括根元类)的父类最终继承自根类对象NSObject
三、方法和消息
1.SEL
typedef struct objc_selector *SEL;
SEL sel1 = @selector(func1);
SEL sel2 = NSSelectorFromString(func2);
SEL本质上是一个指向方法的指针。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识即SEL,每一个方法都对应着一个SEL。所以即使返回值类型或参数类型不同,方法名相同也会报错。
2.IMP
id (*IMP)(id, SEL,...)
IMP的本质是函数指针,指向方法实现的地址,直接通过IMP就可以找到各个方法。这样效率更高,因为绕过了消息传递阶段,直接定位。
SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP。
消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。
3.Method
typedef struct objc_method *Method
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
可以看出method包含SEL和IMP,在实现方法交换时,主要原理就是交换SEL和IMP的映射关系。
参考:https://www.jianshu.com/p/46dd81402f63
https://www.jianshu.com/p/adf0d566c887
https://www.jianshu.com/p/13457a27624c