一、runtime简介
- Objective-C是一种动态语言,其中最主要的是消息机制。理解 Objective-C 的 runtime 机制可以适当的时候对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。
- Objective-C中一切皆对象,我们知道一个类被初始化成一个实例,这个实例是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。
二、runtime详解
Objective-C中有这样几个文件,这里主要介绍runtime.h和message.h这两个文件中的相关方法使用。runtime.h
是运行时最重要的文件,其中包含了对运行时进行操作的方法。在message.h
中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。
1、类
Class代表一个类,它在objc.h中这样定义的
/// objc.h
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
下面我们来看看objc_class
到底是什么,在runtime.h中这样定义的
struct objc_class {
// 好熟悉啊,实例的isa指向类对象,类对象的isa指向元类
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;
类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),类对象的isa指针指向的我们称之为元类。
2、实例
objc.h中这样定义的
/// 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指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。这个时候再来看一个经典图:
来捋一捋这个逻辑:
objc_object
结构体实例它的isa指针指向类对象,类对象的isa
指针指向了元类,super_class
指针指向了父类的类对象,而元类的super_class
指针指向了父类的元类,那元类的isa
指针又指向了自己
3、方法
struct objc_method {
// 方法名
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
// 方法类型
char * _Nullable method_types OBJC2_UNAVAILABLE;
// 方法实现
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
- SEL
objc.h
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。两个类之间,只要方法名相同,那么方法的SEL就是一样的。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法。
不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
在寻找IMP的地址时,runtime提供了两种方法:
IMP class_getMethodImplementation(Class cls, SEL name);
IMP method_getImplementation(Method m)
- IMP
objc.h
/// 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
第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
第二个参数:是方法选择器(selector)
接下来的参数:方法的参数列表。
4、发送消息
在Objective-C中,消息直到运行时才绑定到方法实现上。
// message.h
objc_msgSend(void /* id self, SEL op, ... */ )
1、objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法列表里面查找方法的selector。
2、如果没有找到selector,objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。
3、依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现,并将该方法添加进入缓存中
4、如果最后没有定位到selector,则会走消息转发。
5、消息转发
当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,就会出现报错:
unrecognized selector sent to instance 0x7f91cfd0c200
如果想避免出现这种情况,可以通过respondsToSelector:
进行校验,但是这里不做介绍了,下面着重介绍消息转发
机制。
- 动态解析
对象在接收到未知的消息时,首先会调用所属类的类方法
+resolveInstanceMethod:(实例方法)或者
+resolveClassMethod:(类方法)。
让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(look)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(look)) { //如果是执行look函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)lookMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void lookMethod(id obj, SEL _cmd) {
NSLog(@"new look");//新的look函数
}
// 打印结果
2020-08-23 13:26:32.342039+0800 ModuleProject[7610:289344] new look
可以看到没有实现look这个函数,但是我们通过class_addMethod
动态添加lookMethod函数,并执行lookMethod这个函数的IMP
。从打印结果看,成功实现了。
如果resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector
。
- 备用接收者
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(look)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(look)) {
return NO;
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(look)) {
return [BViewController new]; //返回BViewController对象,让BViewController对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
打印结果
2020-08-23 13:36:17.753347+0800 ModuleProject[7787:297279] new look
可以看到我们通过forwardingTargetForSelector把当前ViewController的方法转发给了BViewController去执行了。打印结果也证明我们成功实现了转发。
如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
- 完整消息转发
如果在上面还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(look)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
// 获得函数的参数和返回值类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"look"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
// 发送消息给目标对象
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
BViewController *b = [BViewController new];
if([b respondsToSelector:sel]) {
[anInvocation invokeWithTarget:b];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
通过签名,runtime生成了一个对象anInvocation,发送给了forwardInvocation,我们在forwardInvocation方法里面让BViewController对象去执行了look函数。