Runtime的特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发,具体怎么实现的呢。我们从下面几个方面探寻Runtime的实现机制。
- Runtime介绍
- Runtime中的数据结构
- Runtime消息传递
- Runtime消息转发
Runtime介绍
静态语言:如C语言,编译阶段就要决定调用哪个函数,如果函数未实现就会编译报错。
动态语言:如OC语言,编译阶段并不能决定真正调用哪个函数,只要函数声明过即使没有实现也不会报错。
我们常说OC是一门动态语言,就是因为它总是把一些决定性的工作从编译阶段推迟到运行时阶段。OC代码的运行不仅需要编译器,还需要运行时系统(Runtime System)来执行编译后的代码。
Runtime是一套底层纯C语言API,OC代码最终都会被编译器转化为运行时代码,通过消息机制决定函数调用方式,这也是OC作为动态语言使用的基础。
Runtime中的数据结构
OC代码被编译器转化成C语言,然后再通过运行时系统执行,最终实现了动态调用。这其中的OC类、对象和方法等都对应了C中的结构体,而且我们都可以在Rutime源码中找到它们的定义。
#import
#import
#import
接下来我们来看一下Runtime主要用到的结构体
一、objc_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;
每个实例内都有一个isa
指针,该指针指向创建该实例的Class
二、objc_class
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
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;
super_class:指向objc_class类所继承的父类的指针。但是如果当前类已经是最顶层的类(如:NSObject),则super_class指针为NULL
name:类的名字
version:版本
info:信息
instance_size:实例大小
ivars:实例变量列表,存取方法都存放在methodLists中
methodLists:方法列表
cache:用于记录每次使用类或者实例对象调用的方法。这样每次响应消息的时候,Runtime系统会优先在cache中寻找响应方法,相比直接在类的方法列表中遍历查找,效率更高。存储的方式是以方法名为key、方法实现为value的Map中
protocols:遵守的协议列表
objc_object
中的isa
指针指向的结构体就是创建该实例的objc_class
。
同时,可以很明显的发现,在objc_class
中也存在着一个isa
指针,那么这个isa
指针指向的又是哪里呢。其实,在Runtime中Objc类本身同时也是一个对象。Runtime把类对象所属类型就叫做元类(Meta-Class)
,用于描述类对象本身所具有的特征,最常见的类方法就被定义于此,所以objc_class
中的isa
指针指向的是元类
,每个实例仅有一个类对象,而每个类对象仅有一个与之相关的元类
。具体的关系我们可以参考下图。
三、objc_method
struct objc_method {
SEL _Nonnull method_name
char * _Nullable method_types
IMP _Nonnull method_imp
}
method_name:方法名,类型SEL
method_types: 一个char指针,指向存储方法的参数类型和返回值类型
method_imp:本质上是一个指针,指向方法的实现
这里其实就是SEL(method_name)
与IMP(method_name)
形成了一个映射,通过SEL
,我们可以很方便的找到方法实现IMP
。
四、SEL
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
SEL
在OC中称作方法选择器,用于表示运行时方法的名字。OC在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
通常我们获取SEL有三种方法:
1.OC中,使用@selector(“方法名字符串”)
2.OC中,使用NSSelectorFromString(“方法名字符串”)
3.Runtime方法,使用sel_registerName(“方法名字符串”)
五、IMP
/// A pointer to the function of a method implementation.
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
IMP
这个函数指针指向了方法实现的首地址,当OC发起消息后,最终执行的代码是由IMP
指针决定的。
六、objc_category
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。
从上面的objc_category
的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性。
平时所说的category中添加属性的操作就是利用Runtime的objc_setAssociatedObject
和objc_getAssociatedObject
来的
Runtime消息传递
在了解完Runtime的数据结构后,我们来看个具体例子,了解下Runtime的消息传递流程是什么样的。比如:[obj foo]
的执行流程
- 通过
obj
的isa
指针,找到对应的Class
- 在
Class
的cache
中查找foo
的IMP
,有则执行IMP
- 没有则到
Class
的method list
中查找foo
,找到执行IMP
- 还没有则到
superClass
中找,有则执行IMP
- 还是没有的话,则会走
Runtime
的消息转发
流程
Runtime消息转发
首先,我们先看一张消息转发的流程图:
一、动态方法解析
所谓动态解析,我们可以理解为通过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)
下面以一个例子来说明动态解析。
Person
类中有+ (void)run:(int)time;
和 - (void)eat:(NSString *)food;
的方法声明但是没有实现,我们通过动态方法解析为其添加实现,具体代码如下:
// ViewController.m
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Person *person = [[Person alloc] init];
[person eat:@"Apple"];
[Person run:10];
}
@end
// Person.h
#import
@interface Person : NSObject
// 只有声明,没有实现
+ (void)run:(int)time;
// 只有声明,没有实现
- (void)eat:(NSString *)food;
@end
// Person.m
#import "Person.h"
#import "objc/runtime.h"
#import "objc/message.h"
@implementation Person
+ (BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(run:))
{
//添加函数实现
class_addMethod(object_getClass(self),
sel,
class_getMethodImplementation(object_getClass(self), @selector(resolve_run:)),
"v@");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat:))
{
//添加函数实现
class_addMethod([self class],
sel,
class_getMethodImplementation([self class], @selector(resolve_eat:)),
"v@");
return YES;
}
return [super resolveInstanceMethod:sel];;
}
+ (void)resolve_run:(int)time
{
NSLog(@"run %d minute", time);
}
- (void)resolve_eat:(NSString *)food
{
NSLog(@"eat %@", food);
}
@end
执行结果如下:
eat Apple
run 10 minute
注意:
- 我们注意到class_addMethod方法中的特殊参数“v@”,具体可参考苹果文档Type Encodings有详细的解释。
- 成功使用动态方法解析还有个前提,那就是我们必须存在可以处理消息的方法,比如上述代码中的
resolve_run:
与resolve_eat:
二、消息接受者重定向
当动态解析过程中没有对方法进行实现的话,那么消息发送机制就进入了消息转发(Forwarding)的阶段了,我们可以使用Runtime通过下面的方法替换消息接收者的为其他对象,从而保证程序的继续执行。
//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
还是上面的例子,我们在Person
中对eat:
方法不进行动态解析,让它进入下一步(接受者重定向
),转由Student
类处理这个事件。
// Person.m
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return [super resolveInstanceMethod:sel];;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(eat:))
{
return [Student new];
}
return [super forwardingTargetForSelector:aSelector];
}
- (void)resolve_eat:(NSString *)food
{
NSLog(@"eat %@", food);
}
// Student.h
#import
@interface Student : NSObject
- (void)eat:(NSString *)food;
@end
// Student.m
#import "Student.h"
@implementation Student
- (void)eat:(NSString *)food
{
NSLog(@"student eat %@", food);
}
@end
执行结果如下:
student eat Apple
可以看到我们通过forwardingTargetForSelector
把当前Person
的方法转发给了Student
去执行了。打印结果也证明我们成功实现了转发。
注意:
在这里只会对实例方法进行接受者重定向,而不会对类方法进行接受者重定向
三、完整消息重定向
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送- methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果- methodSignatureForSelector:
返回nil
,Runtime
则会发出- doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime
就会创建一个NSInvocation
对象并发送- forwardInvocation:
消息给目标对象。
下面看一个例子,还是之前的eat:
方法,Person
类没有实现,我们通过完整消息重定向的方式让Teacher
和Farmer
两个类来实现这个方法。
// Person.m
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(eat:))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL sel = anInvocation.selector;
Teacher *teacher = [Teacher new];
Farmer *farmer = [Farmer new];
if ([teacher respondsToSelector:sel] || [farmer respondsToSelector:sel])
{
if ([teacher respondsToSelector:sel])
{
[anInvocation invokeWithTarget:teacher];
}
if ([farmer respondsToSelector:sel])
{
[anInvocation invokeWithTarget:farmer];
}
}
else
{
[self doesNotRecognizeSelector:sel];
}
}
// Teacher.h
#import
@interface Teacher : NSObject
- (void)eat:(NSString *)food;
@end
// Teacher.m
#import "Teacher.h"
@implementation Teacher
- (void)eat:(NSString *)food
{
NSLog(@"Teacher eat %@", food);
}
@end
// Farmer.h
#import
@interface Farmer : NSObject
- (void)eat:(NSString *)food;
@end
// Farmer.m
#import "Farmer.h"
@implementation Farmer
- (void)eat:(NSString *)food
{
NSLog(@"Farmer eat %@", food);
}
@end
打印结果如下:
Teacher eat Apple
Farmer eat Apple
总结:
- 从以上的代码中就可以看出,
- forwardingTargetForSelector
仅支持一个对象的返回,也就是说消息只能被转发给一个对象,而- forwardInvocation
可以将消息同时转发给任意多个对象,这就是两者的最大区别。 - 虽然理论上可以重载doesNotRecognizeSelector函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。
参考自:
Runtime-iOS运行时基础篇
iOS Runtime详解