>Runtime 介绍
C 语言 作为一门静态类语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现。而 Objective-C 语言是一门动态语言。在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪个函数。只有在运行时间才检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。这样在程序没运行的时候,我们并不知道调用一个方法具体会发生什么。
Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。
Runtime其实有两个版本: modern
和legacy
。我们现在用的 Objective-C 2.0
采用的是现行 (Modern
) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 macOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy
版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
Runtime
基本是用 C
和汇编
写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。平时的业务中主要是使用官方Api,解决我们框架性的需求。
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC
并不能直接编译为汇编语言,而是要先转写为纯C
语言再进行编译和汇编的操作,从OC
到C
语言的过渡就是由runtime来实现的。然而我们使用OC
进行面向对象开发,而C
语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
>数据结构
runtime数据结构.png>消息机制的基本原理
基本消息发送框架.pngObjective-C 语言 中,对象方法调用都是类似[receiver selector];
的形式,其本质就是让对象在运行时发送消息的过程。编译器转成消息发送objc_msgSend(receiver, selector)
或objc_msgSend(recevier,selector,org1,org2,…)
,runtime时执行的流程是这样的:
- 通过
recevier
的isa 指针
找到recevier
的Class(类)
; - 在
Class
的cache(方法缓存)
的散列表中寻找对应的IMP(方法实现)
; - 如果在
cache
中没有找到对应的IMP
,就继续在Class
的method list(方法列表)
中找对应的selector
,如果找到则填充到cache
中,并返回selector
; - 如果在
Class
中没有找到这个selector
,就继续在它的superClass(父类)
中寻找; - 一旦找到对应的
selector
,直接执行recevier
对应selector
方法实现的IMP
。 - 若找不到对应的
selector
,消息被转发或者临时向recevier
添加这个selector
对应的实现方法,否则就会发生崩溃。
但这种实现有个问题,效率低。一个class往往只有20%
的函数会被经常调用,可能占总调用次数的80%
。每个消息都需要遍历一次objc_method_list
并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class
中另一个重要成员objc_cache
做的事情 - 再找到selector
之后,把selector
的method_name
作为key
,method_imp
作为value
给存起来。当再次收到recevier
消息的时候,可以直接在cache
里找到,避免去遍历objc_method_list
。从前面的源代码可以看到objc_cache
是存在objc_class
结构体中的。
objec_msgSend
的方法定义如下:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
>Runtime 中的概念解析
-
类对象(objc_class)
struct objc_class {
// objc_class 结构体的实例指针
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
// 指向父类的指针
Class _Nullable super_class OBJC2_UNAVAILABLE;
// 类的名字
const char * _Nonnull name OBJC2_UNAVAILABLE;
// 类的版本信息,默认为 0
long version OBJC2_UNAVAILABLE;
// 类的信息,供运行期使用的一些位标识
long info OBJC2_UNAVAILABLE;
// 该类的实例变量大小;
long instance_size OBJC2_UNAVAILABLE;
// 该类的实例变量列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
// 方法定义的列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 方法缓存
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
// 遵守的协议列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
类对象
就是一个结构体struct objc_class
,这个结构体存放的数据称为元数据(metadata)
,该结构体的第一个成员变量也是isa指针
,这就说明了Class
本身其实也是一个对象
,因此我们称之为类对象
,类对象在编译期产生用于创建实例对象,是单例。
-
实例(objc_object)
Object(对象)
被定义为objc_object
结构体,其数据结构如下:
/// Represents an instance of a class.
struct objc_object {
// objc_object 结构体的实例指针
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
这里的id
被定义为一个指向objc_object 结构体
的指针。从中可以看出objc_object 结构体
只包含一个Class
类型的isa 指针
。换句话说,一个Object(对象)
唯一保存的就是它所属 Class(类)
的地址。当我们对一个对象,进行方法调用时,比如[receiver selector];
,它会通过objc_object 结构体
的isa 指针
去找对应的object_class 结构体
,然后在object_class 结构体
的 methodLists(方法列表)
中找到我们调用的方法,然后执行。
-
元类(Meta Class)
对象(objc_object 结构体)
的isa 指针
指向的是对应的类对象(object_class 结构体)
。而类对象(object_class 结构体)
的isa 指针
实际上指向的的是类对象
自身的Meta Class(元类)
。
Meta Class(元类)
就是一个类对象所属的类。一个对象所属的类叫做类对象
,而一个类对象所属的类就叫做元类
。
Runtime 中把类对象所属类型就叫做
Meta Class(元类)
,用于描述类对象本身所具有的特征,而在元类
的methodLists
中,保存了类的方法链表,即所谓的类方法
。并且类对象
中的isa 指针
指向的就是元类
。每个类对象有且仅有一个与之相关的元类。
对象方法的调用过程,我们是通过对象的isa 指针
找到对应的 Class(类)
;然后在Class(类)
的method list(方法列表)
中找对应的selector
。而 类方法的调用过程 和对象方法调用差不多,流程如下:
- 通过
类对象
的isa 指针
找到所属的Meta Class(元类)
; - 在
Meta Class(元类)
的method list(方法列表)
中找到对应的selector
; - 执行对应的
selector
。
下面我们通过一张图来清晰地表示出实例对象(Object)
、类(Class)
、Meta Class(元类)
的指向关系。
通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object结构体
实例它的isa指针
指向类对象
,类对象
的isa指针
指向了元类
,super_class指针
指向了父类的类对象
,而元类
的super_class指针
指向了父类的元类
,那元类的isa指针
又指向了自己。
元类(Meta Class)
是一个类对象的类。
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即)。为了调用类方法,这个类的isa指针
必须指向一个包含这些类方法的一个objc_class结构体
。这就引出了meta-class
的概念,元类中保存了创建类对象以及类方法所需的所有信息。任何NSObject
继承体系下的meta-class
都使用NSObject
的meta-class
作为自己的所属类,而基类的meta-class
的isa指针
是指向它自己。
@interface NSObject (Test)
+ (void)foo;
@end
@implementation NSObject (Test)
- (void) foo {
NSLog(@"%@",NSStringFromSelector(_cmd));
return;
}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
[NSObject foo];
[[NSObject new] foo];
}
return 0;
}
输出结果:
foo
foo
解析:类的实例方法是存储在类的
methodLists
中,而类方法则是存储在元类的methodLists
中,NSObject
的元类
的superclass
是指向Class
,当调用[NSObject foo]
的时候,因为这是一个类方法调用,所以从元类
中查找签名为foo
的方法,没有发现,然后再沿superclass
继续查找,结果在Class
中查找到该方法,于是调用该方法输出。但如果将NSObject
的分类换成其他类的分类(如NSString
),会发现程序崩溃,这是因为签名为foo
的函数在NSString
中,而当我们进行类方法调用的时候,最后会查找到NSObject
的Class
中,但该Class
中并没有对应的方法签名,于是再沿superclass
向上查找,由于NSObject
的superclass
是nil
,于是抛出unrecognized selector
。
-
Method(objc_method)
object_class 结构体
的methodLists(方法列表)
中存放的元素就是Method(方法)
。在objc/runtime.h
中,表示Method(方法)
的objc_method 结构体
的数据结构:
/// An opaque type that represents a method in a class definition.
/// 代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
// 方法名
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
// 方法类型
char * _Nullable method_types OBJC2_UNAVAILABLE;
// 方法实现
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
objc_method 结构体
中包含了method_name(方法名)
,method_types(方法类型)
和method_imp(方法实现)
。说明SEL
和IMP
其实都是Method
的属性。下面,我们来了解下这三个变量。
1. SEL(objc_selector)
Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;
objc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objective-C中
的表示类型(Swift
中是Selector
类)。selector
是方法选择器,可以理解为区分方法的ID
,而这个ID
的数据结构是SEL
。SEL
是一个指向objc_selector 结构体
的指针。
在runtime
相关头文件中并没有找到明确的定义。不过,通过测试我们可以得出:SEL
只是一个保存方法名的字符串。
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel); // 输出:viewDidLoad
SEL sel1 = @selector(test);
NSLog(@"%s", sel1); // 输出:test
@property SEL selector;
可以看到selector
是SEL
的一个实例。
A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.
其实selecto
r就是个映射到方法的C
字符串,你可以用Objective-C
编译器命令@selector()
或者Runtime
系统的sel_registerName
函数来获得一个SEL
类型的方法选择器。
selector
既然是一个string
,是类似className+method
的组合,命名规则有两条:
- 同一个类,selector不能重复
- 不同的类,selector可以重复
这也带来了一个弊端,我们在写C
代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C
中是行不通的,因为selector
只记了method
的name
,没有参数,所以没法区分不同的method
。
比如:
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;
是会报错的。
我们只能通过命名来区别:
- (void)caculateWithInt(NSInteger)num;
- (void)caculateWithFloat(CGFloat)num;
在不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。
2. IMP(method_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
用来找到函数地址,然后执行函数。在iOS
的Runtime
中,Method
通过selector
和IMP
两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。
IMP和SEL关系:
每一个继承于NSObject
的类都能自动获得runtime
的支持。在这样的一个类中有一个isa指针
指向该类定义的数据结构体,这个结构体是由编译器编译时为类(需继承于NSObject
)创建的。在这个结构体中有包括了指向其父类类定义的指针以及 Dispatch table
。Dispatch table
是一张SEL
和IMP
的对应表。也就是说方法编号SEL
最后还是要通过Dispatch table
表寻找到对应的IMP
,IMP
就是一个函数指针,然后执行这个方法:
1)通过方法获得方法的编号:SEL methodId=@selector(methodName);
或者SEL methodId = NSSelectorFromString(methodName);
2)通过方法编号执行该编号的方法:[self performSelector:methodId withObject:nil];
3)通过方法编号获取该编号的方法名 NSString *methodName = NSStringFromSelector(methodId);
4)通过方法编号获得IMP
:IMP methodPoint = [self methodForSelector:methodId];
5)执行IMP
:void (*func)(id, SEL, id) = (void *)imp;
、func(self, methodName,param);
注意:如果方法没有传入参数时:
void (*func)(id, SEL) = (void *)imp;
、func(self, methodName);
。如果方法传入一个参数时:void (*func)(id, SEL,id) = (void *)imp;
、func(self, methodName,param);
。如果方法传入俩个参数时:void (*func)(id, SEL,id,id) = (void *)imp;
、func(self, methodName,param1,param2);
。
想更深入了解 IMP 的小伙伴请戳这里。
3. char *method_types
方法类型method_types
是个字符串,用来存储方法的参数类型和返回值类型。
-
类缓存(objc_cache)
当Objective-C
运行时通过跟踪它的isa 指针
检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend
查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache
,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime
系统实际上非常快,接近直接执行内存地址的程序速度。
-
Runtime消息转发
进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject
),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:
方法报unrecognized selector
错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。
- 动态方法解析(消息动态解析)
- 备用接收者(消息接受者重定向)
- 完整消息转发(消息重定向)
1.动态方法解析(消息动态解析)
首先,Objective-C
运行时会调用+resolveInstanceMethod:
或者+resolveClassMethod:
,让你有机会提供一个函数实现。前者在对象方法
未找到时调用,后者在类方法
未找到时调用。我们可以通过重写这两个方法,添加其他函数实现,并返回YES
, 那运行时系统就会重新启动一次消息发送的过程。
主要用的的方法如下:
// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
/**
* class_addMethod 向具有给定名称和实现的类中添加新方法
* @param cls 被添加方法的类
* @param name selector 方法名
* @param imp 实现方法的函数指针
* @param types imp 指向函数的返回值与参数类型
* @return 如果添加方法成功返回 YES,否则返回 NO
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp,
const char * _Nullable types);
举个例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 执行 fun 函数
[self performSelector:@selector(fun)];
}
// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMP
class_addMethod([self class], sel, (IMP)funMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void funMethod(id obj, SEL _cmd) {
NSLog(@"funMethod"); //新的 fun 函数
}
@end
输出结果:
funMethod
从上边的例子中,我们可以看出,虽然我们没有实现fun
方法,但是通过重写resolveInstanceMethod:
,利用class_addMethod
方法添加对象方法实现funMethod
方法并执行。从打印结果来看,成功调起了funMethod
方法。
我们注意到 class_addMethod 方法中的特殊参数
v@:
,具体可参考官方文档中关于Type Encodings
的说明:传送门
2.备用接收者(消息接受者重定向)
如果上一步中+resolveInstanceMethod:
或者+resolveClassMethod:
没有添加其他函数实现,运行时就会进行下一步——消息接受者重定向。
如果当前对象实现了-forwardingTargetForSelector:
或者 +forwardingTargetForSelector:
方法,Runtime
就会调用这个方法,允许我们将消息的接受者转发给其他对象。
其中用到的方法:
// 重定向类方法的消息接收者,返回一个类或实例对象
+ (id)forwardingTargetForSelector:(SEL)aSelector;
// 重定向方法的消息接收者,返回一个类或实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector;
注意:
1.类方法和对象方法消息转发第二步调用的方法不一样,前者是+forwardingTargetForSelector:
方法,后者是-forwardingTargetForSelector:
方法。
2.这里+resolveInstanceMethod:
或者+resolveClassMethod:
无论是返回YES
还是NO
,只要其中没有添加其他函数实现,运行时都会进行下一步。
举个例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 执行 fun 方法
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 为了进行下一步 消息接受者重定向
}
// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(fun)) {
return [[Person alloc] init];
// 返回 Person 对象,让 Person 对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
输出结果:
fun
可以看到,虽然当前ViewController
没有实现fun
方法,+resolveInstanceMethod:
也没有添加其他函数实现。但是我们通过forwardingTargetForSelector
把当前ViewController
的方法转发给了Person
对象去执行了。打印结果也证明我们成功实现了转发。
我们通过forwardingTargetForSelector
可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是nil
,也不是self
,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步——消息重定向流程。
3.完整消息转发(消息重定向)
如果经过消息动态解析、消息接受者重定向,Runtime
系统还是找不到相应的方法实现而无法响应消息,Runtime
系统会利用-methodSignatureForSelector:
或者 +methodSignatureForSelector:
方法获取函数的参数和返回值类型。
如果methodSignatureForSelector:
返回了一个 NSMethodSignature 对象(函数签名)
,Runtime
系统就会创建一个NSInvocation
对象,并通过forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找IMP
的机会。如果methodSignatureForSelector:
返回nil
。则Runtime
系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了。所以我们可以在forwardInvocation:
方法中对消息进行转发。
注意:类方法和对象方法消息转发第三步调用的方法同样不一样。
- 类方法调用的是:
1.methodSignatureForSelector:
2.forwardInvocation:
3.doesNotRecognizeSelector:
- 对象方法调用的是:
1.methodSignatureForSelector:
2.forwardInvocation:
3.doesNotRecognizeSelector:
用到的方法:
// 获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;
// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
举个例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 执行 fun 函数
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 为了进行下一步 消息接受者重定向
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; // 为了进行下一步 消息重定向
}
// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector; // 从 anInvocation 中获取消息
Person *p = [[Person alloc] init];
if([p respondsToSelector:sel]) { // 判断 Person 对象方法是否可以响应 sel
[anInvocation invokeWithTarget:p]; // 若可以响应,则将消息转发给其他对象处理
} else {
[self doesNotRecognizeSelector:sel]; // 若仍然无法响应,则报错:找不到响应方法
}
}
@end
输出结果:
fun
可以看到,我们在-forwardInvocation:
方法里面让Person 对象
去执行了fun
函数。
既然-forwardingTargetForSelector:
和-forwardInvocation:
都可以将消息转发给其他对象处理,那么两者的区别在哪?区别就在于-forwardingTargetForSelector:
只能将消息转发给一个对象。而-forwardInvocation:
可以将消息转发给多个对象。
以上就是 Runtime 消息转发的整个流程。
参考链接:
https://www.jianshu.com/p/6ebda3cd8052
https://www.jianshu.com/p/633e5d8386a8
https://www.imooc.com/article/38310
官方文档:
runtime源码地址(开源)
苹果官方Runtime编程指南
objc_msgSend
objc_msgSend_fpret
objc_msgsend_stret
objc_msgsendsuper
objc_msgsendsuper_stret
大神入口:
http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/
网友评论