阅读本文将了解下面几个问题:
- 什么是 Runtime?
- 消息机制的基本原理
- Runtime中的数据结构
- Runtime消息发送原理
- 消息发送流程总结
1 什么是 Runtime?
C/C++ 是静态语言的代表,它们在编译阶段就已经确定好了要调用的函数,以及函数的实现,如果函数未实现就会编译报错。
Objective-C 是一个动态语言,在编译阶段并不知道真正调用的是哪个函数。只有在运行时才知道具体会发生什么。
Objective-C
的动态性,决定了它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。这个运行时系统就是Runtime
,它基本上是用C
和汇编写的一套API
,这个库使C
语言有了面向对象的能力。
2 消息机制的基本原理
OC
的方法调用都是类似[receiver selector]
的形式,其实每次都是一个运行时消息发送过程。
编译阶段:
[receiver selector]
方法被编译器转化:
1.objc_msgSend(receiver,selector)
(不带参数)
2.objc_msgSend(recevier,selector,org1,org2,…)
(带参数)运行时阶段:消息接收者
recever
寻找对应的selector:
1.recever
能找到对应的selector
,直接执行接收receiver
对象的selector
方法。
2.recever
找不到对应的selector
,消息被转发或者临时向recever
添加这个selector
对应的实现内容,否则崩溃。
OC
调用方法[receiver selector]
,编译阶段确定了要向哪个接收者发送message
消息,但是接收者如何响应决定于运行时的判断。
Objective-C
代码在编译和运行阶段会被转化为Runtime
方式运行,Objective-C
类、对象和方法等都对应了C
中的结构体,我们来看一下它们是如何定义的。
3 Runtime中的数据结构
Runtime
代码如何查看呢,我们可以通过下面的方式:
1.导入下面的头文件,然后Command +鼠标右键点击
,即可进入Runtime
的源码文件。
#import <objc/runtime.h>
2.我们也可以通过组合键 [Cmd + Shift + O ]
,搜索相应的文件进入。
下面我们就分析一下Objective-C
代码在C
中对应的结构。
3.1 objc_msgSend
上面也提到过,Objective-C
方法调用在编译时都会转化为对应C
函数的调用:objc_msgSend(receiver,selector)
。
3.2 Object(实例)
objc/objc.h
中,我们来看一下Object
(对象),是如何定义的:
/// 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;
我们知道id
是一种通用的对象类型,它可以指向属于任何类的对象。在这里id
被定义为一个指向 objc_object
结构体的指针。
而objc_object
结构体只包含一个Class
类型的isa
指针,也就是说,一个Object
(对象)唯一保存的就是它所属Class
(类) 的地址。下面我们看一下 Class
是如何定义的。
3.3 Class(类)
在objc/objc.h
中,可以看到Class
是一个指向objc_class
结构体的指针:
typedef struct objc_class *Class;
在objc/runtime.h
中,是objc_class
结构体的具体定义:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class // 指向父类的指针;
const char * _Nonnull name // 类名;
long version // 类的版本信息,默认为 0;
long info // 类的信息,供运行期使用的一些位标识;
long instance_size // 该类的实例变量大小;
struct objc_ivar_list * _Nullable ivars // 该类的实例变量列表;
struct objc_method_list * _Nullable * _Nullable methodLists // 方法定义列表 ;
struct objc_cache * _Nonnull cache // 方法缓存;
struct objc_protocol_list * _Nullable protocols // 遵守的协议列表;
#endif
} OBJC2_UNAVAILABLE;
我们可以看到objc_class
结构体中定义了很多的成员变量:指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等。这个结构体存放的数据称为元数据(metadata
)。
我们还能注意到objc_class
结构体中也有一个isa
指针。这就说明了Class
本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生,用于创建实例对象,是单例。
3.4 元类(Meta Class):
我们可以发现实例对象和类对象结构体中都拥有一个isa
指针,实例对象的isa
指针指向他所属的类(Class
),那么类对象的isa
指针指向哪儿里呢?
类对象的isa
指针指向了元类,元类(Meta Class
)是一个类对象的类。在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
为了调用类方法,这个类的isa
指针必须指向一个包含这些类方法的一个objc_class
结构体。这就引出了meta-class
的概念,元类中保存了创建类对象以及类方法所需的所有信息。
3.5 实例对象、类、元类之间的关系
上面,我们已经了解了 实例对象(Object
)、类(Class
)、Meta Class
(元类) 的基本概念。
下面,我们通过一张图,来清晰的了解下它们之间的继承关系,以及isa
的指向关系:
isa
指针指向:
- 实例对象的
isa
指针指向了对应的类对象,而类对象的isa
指针指向了对应的元类。 - 所有元类的isa指针最终指向了
NSObject
元类,因此NSObject
元类也被称为根元类。根元类的isa
指针又指向了自己。
super_class
指针指向:
- 类对象的
super_class
指针指向了父类对象,父类对象又指向了根类对象,根类对象最终指向了nil
。 - 元类的
super_class
指针指向了父元类。父元类又指向了根元类。而根元类的super_class
指针指向了根类对象,最终指向了nil
。
3.6 Method(方法)
object_class
中methodLists
(方法列表)存放的元素就是Method
(方法)。
在objc/runtime.h
中,看下定义:
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name // 方法名;
char * _Nullable method_types // 方法类型;
IMP _Nonnull method_imp // 方法实现;
}
下面,我们来了解下它的三个成员变量。
1. SEL(方法名)
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
SEL
是一个指向objc_selector
结构体的指针,然而我们并不能在Runtime
中找到它的结构体的详细定义。Objective-C
在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int
类型的地址),这个标识就是SEL
。
2. IMP(方法实现)
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
IMP
的实质是一个函数指针,它指向了方法实现的首地址。IMP
用来找到函数地址,然后执行函数。
3. char * method_types (方法类型)
方法类型method_types
是个字符串,用来存储方法的参数类型和返回值类型。
到这里,Method
的结构就已经很清楚了,Method
将SEL
(方法名) 和IMP
(函数指针)关联起来,当对一个对象发送消息时,会通过给出的SEL
(方法名)去找到IMP
(函数指针),然后执行。
3.7 类缓存(objc_cache)
objc_cache
用于缓存最近使用的方法。一个类只有一部分方法是常用的,每次调用一个方法之后,这个方法就被缓存到objc_cache
中,下次调用时runtime
会先在objc_cache
中查找,如果objc_cache
中没有,才会去methodList
中查找。相比直接在类的方法列表中遍历查找,效率更高。
4 深入理解Rutime消息发送
我们在分析了OC
语言对应的底层C
结构之后,现在可以进一步理解运行时的消息发送机制。先前讲到,OC
调用方法被编译转化为如下的形式:
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
最后一步中我们提到:若找不到对应的selector
,消息被转发或者临时向recevier
添加这个selector
应的实现方法,否则就会发生崩溃。
当一个方法找不到的时候,Runtime
提供了 消息动态解析、消息接受者重定向、消息重定向 三种方法来处理,这三种方法的调用关系如下图:
4.1 动态方法解析(Dynamic Method Resolution)
所谓动态解析,我们可以理解为通过cache
和方法列表没有找到方法时,Runtime
为我们提供一次动态添加方法实现的机会,主要用到的方法如下:
//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel
//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
//Runtime方法:
/**
运行时方法:向指定类中添加特定方法实现的操作
@param cls 被添加方法的类
@param name selector方法名
@param imp 指向实现方法的函数指针
@param types imp函数实现的返回值与参数类型
@return 添加方法是否成功
*/
BOOL class_addMethod(Class _Nullable cls,
SEL _Nonnull name,
IMP _Nonnull imp,
const char * _Nullable types)
下面使用一个示例来说明动态解析:ViewController
类中声明方法却未添加实现,我们通过Runtime
动态方法解析的操作为其添加方法实现,具体代码如下:
#import "ViewController.h"
#import <objc/runtime.h>
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[self class] performSelector:@selector(run)];
[self performSelector:@selector(walk)];
}
//重写父类方法:处理类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
if(sel == @selector(run)){
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(ssl_run)), "v@");
return YES; //添加函数实现,返回YES
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
//重写父类方法:处理实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if(sel == @selector(walk)){
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(ssl_walk)), "v@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
+ (void)ssl_run {
NSLog(@"%s ",__func__);
}
- (void)ssl_walk {
NSLog(@"%s ",__func__);
}
运行程序,代码没有崩溃,并打印如下结果:
+[ViewController ssl_run]
-[ViewController ssl_walk]
class_addMethod 方法中的特殊参数“v@”,具体可参考官方文档中关于 Type Encodings 的说明:点击查看
这里+resolveInstanceMethod:
或者 +resolveClassMethod:
无论是返回YES
,还是返回NO
,只要其中没有添加其他函数实现,Runtime
都会进行下一步:消息接受者重定向。
4.2 消息接收者重定向
这一步会调用下面两个方法:
//重定向类方法的消息接收者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector
//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
如果当前对象实现了这两个方法,Runtime
就会调用这两个方法,允许我们将消息的接受者转发给其他对象。
下面使用一个示例来说明消息接收者的重定向:
#import "ViewController.h"
#import <objc/runtime.h>
@interface Person : NSObject
@end
@implementation Person
+ (void)run {
NSLog(@"%s ",__func__);
}
- (void)walk {
NSLog(@"%s ",__func__);
}
@end
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[self class] performSelector:@selector(run)];
[self performSelector:@selector(walk)];
}
//重定向类方法:返回一个类对象
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
return [Person class];
}
return [super forwardingTargetForSelector:aSelector];
}
//重定向实例方法:返回类的实例
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(walk)) {
return self.person;
}
return [super forwardingTargetForSelector:aSelector];
}
- (Person *)person {
if (!_person) {
_person = [Person new];
}
return _person;
}
@end
代码没有崩溃,并打印如下结果:
+[ViewController ssl_run]
-[ViewController ssl_walk]
动态方法解析阶段无效时,我们可以通过forwardingTargetForSelector
修改消息的接收者,该方法返回参数是一个对象,如果这个对象是非nil
,非self
,系统会将运行的消息转发给这个对象执行。否则,进行下一步:消息重定向。
4.3 消息重定向
这一步中首先会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:
返回nil
,Runtime
则会发出-doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime
就会创建一个NSInvocation
对象并发送 -forwardInvocation:
消息给目标对象。
看下方法的定义:
// 获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;
// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
举个例子:
#import "ViewController.h"
#import <objc/runtime.h>
@interface Person : NSObject
@end
@implementation Person
- (void)walk {
NSLog(@"%s ",__func__);
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 执行walk函数
[self performSelector:@selector(walk)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;// 返回YES,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;// 返回nil,进入下一步转发
}
// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"walk"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;// 从anInvocation中获取消息
Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];// 将消息转发给其他对象处理
}
else {
[self doesNotRecognizeSelector:sel];// 报错,代码崩溃
}
}
@end
代码没有崩溃,并打印如下结果:
-[Person walk]
这一步中,通过签名,Runtime
生成了一个对象anInvocation
,发送给了forwardInvocation
,我们在forwardInvocation
方法里面让Person
对象去执行了walk
函数。
以上就是Runtime
的三次转发流程。
5 消息发送流程总结
调用[receiver selector]
后,进行的流程:
-
编译阶段:
[receiver selector]
方法被编译器转化:-
objc_msgSend(receiver,selector)
(不带参数)。 -
objc_msgSend(recevier,selector,org1,org2,…)
(带参数)。
-
-
运行时阶段:
recevier
寻找对应的selector
:- 通过
recevier
的isa
指针找到recevier
的class
(类)。 - 在
Class
(类)的cache
(方法缓存)中寻找对应的selector
。 - 如果在
cache
(方法缓存)中没有找到对应的selector
,就继续在Class
(类)的methodList
(方法列表)中查找,如果找到,缓存到cache
中,并返回selector
。 - 如果在
class
(类)中没有找到这个selector
,就继续在它的superclass
(父类)中寻找。 - 一旦找到
selector
,直接执行相关联的IMP
(方法实现)。 - 若找不到对应的
selector
,Runtime
系统进入消息转发阶段。
- 通过
-
消息转发阶段:
- 动态解析:通过重写
+resolveInstanceMethod:
或者+resolveClassMethod:
方法,利用class_addMethod
方法添加其他函数实现。 - 消息接受者重定向:如果上一步没有添加其他函数实现,可在当前对象中利用
forwardingTargetForSelector:
方法将消息的接受者转发给其他对象。 - 消息重定向:如果上一步返回值为
nil
,则利用methodSignatureForSelector:
方法获取函数的参数和返回值类型。- 如果
methodSignatureForSelector:
返回nil
。则Runtime
系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了。 - 如果
methodSignatureForSelector:
返回了一个函数签名,Runtime
就会创建一个NSInvocation
对象并发送-forwardInvocation:
消息给目标对象。
- 如果
- 动态解析:通过重写