最近开始学习Runtime相关的内容.之前的话知道OC是一门动态的语言(动态类型,动态绑定,动态加载),却不知道到底是些什么东西,了解过Runtime之后,对OC的了解更加深刻了,感觉什么都是动态的了.
在了解消息转发机制之前,我们先要知道以下几个概念:SEL, IMP, Method
SEL
SEL又叫做方法选择器,是一个方法的selector指针,定义如下
typedef struct objc_selector *SEL;
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;// 名称
char *types; OBJC2_UNAVAILABLE;// 类型
};
@selector(test)里面的test表示方法运行时的名字.OC在编译时,会依据每一个方法的名字、序列,生成一个唯一的整形标识(Int类型的地址),这个标识就是SEL
SEL test = @selector(test);
两个类之间,无论有没有父子关系或者兄弟关系,只要方法名相同,那么方法的SEL就是一样的.每一个方法都对应着一个SEL,所以在OC中同一个类(及该类的子类或者父类中),不能存在两个同名的方法,即使参数的类型不同也不行.相同的方法只能对应一个SEL.
工程中的SEL会组成一个set集合,set里面的元素是不能重复的,所以里面的SEL都是唯一的.因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,所以说速度上比较快
本质上,SEL只是一个指向方法的指针(准确的来说,只是一个根据方法名hash化了的KEY值),它的存在是为了加快方法的查询速度
IMP
IMP(implementation的缩写)不是打LOL的那个imp,他实际上是一个函数指针,指向方法实现的首地址,定义如下
typedef id (*IMP)(id, SEL, ...);
- id----指向self的指针(如果是实例方法,则是实例的内存地址;如果是类方法,则是指向元类的指针)
- SEL----方法选择器
- ...----方法的实际参数列表
前面说过,每一个方法都只有唯一的SEL,因此我们可以通过SEL方便快速准确的获得他所对应的IMP. 取得IMP之后,我们就获得了执行这个方法代码的入口点,此时,我们就可以向调用普通的C函数语言一样来使用这个函数指针了.
当取得IMP之后,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样就省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些.
Method
Method的定义如下:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE; // 方法类型
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
方法操作
Runtime提供了一系列的方法来处理与方法相关的操作。包括方法本身及SEL。本节我们介绍一下这些函数。
// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
基本消息传递
在OC中,对象调用方法叫做发送消息. 在编译时,程序的源代码会将从消息发送消息转化成Runtime中objc_msgSend函数调用.
id objc_msgSend(id self, SEL op, ...)
- self----一个指向接收消息类的实例指针
- op-----消息的SEL
- ...------该方法包含的参数
当发生一个objc_msgsend函数调用时,编译器会根据函数类型自动转化成下列的某一个函数
- objc_msgSend 普通的消息都会通过该函数发送
- objc_msgSend_stret 消息中有数据结构作为返回值(不是简单值)时,通过此函数发送和接收返回值
- objc_msgSendSuper 这里把消息发送给父类的实例
- objc_msgSendSuper_stret 这里把消息发送给父类的实例并接收返回值
需要注意的地方:
像objc_msgSend(self, sayHello, @"大家好!");
这样子调用objc_msgSend函数可能会以下的错误:
报错Too many arguments to function call ,expected 0,have3
直接通过objc_msgSend(self, setter, value)
会报错,说参数过多。请这样解决:Build Setting–> Apple LLVM 7.0 – Preprocessing–> Enable Strict Checking of objc_msgSend Calls 改为 NO
当然你也可以这样(推荐):
((void (*)(id, SEL, id))objc_msgSend)(self, sayHello, @"大家好");
强制转换objc_msgSend函数类型为带三个参数且返回值为void函数,然后才能传三个参数。
objc_msgSend函数的调用过程:
- 第一步:检测这个selector是不是要忽略的。
- 第二步:检测这个target是不是nil对象。nil对象发送任何一个消息都会被忽略掉。
- 第三步:
- 1.调用实例方法时,它会首先在自身isa指针指向的类methodLists中查找该方法,如果找不到则会通过class的super_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根class;
- 2.当我们调用某个某个类方法时,它会首先通过自己的isa指针找到metaclass,并从其中methodLists中查找该类方法,如果找不到则会通过metaclass的super_class指针找到父类的metaclass对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根metaclass;
- 第四部:前三部都找不到就会进入动态方法解析.
消息的动态解析
动态解析流程图(图片来自网络):
- 第一步: 通过
resolveInstanceMethod:
方法决定是否动态添加方法。如果返回Yes则通过class_addMethod()
动态添加方法,消息得到处理,结束;如果返回No,则进入下一步(开始动态方法解析); - 第二步: 这步会进入
forwardingTargetForSelector:
方法,用于指定备选对象响应这个消息,不能指定为self(会出现死循环)。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入第三步; - 第三部: 这步我们要通过
methodSignatureForSelector:
方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入第四步; - 第四部: 这步调用
forwardInvocation:
方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector
方法,若我们没有实现这个方法,那么就会crash。
第一步:动态方法解析
当对象接受到未知消息或者该方法还没有实现的时候,就会调用所属类的类方法+(Bool)resolveInstanceMethod:
在这个Demo里面的Person.h文件代码如下
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)play;
@end
Person.m文件里面的代码如下:
#import "Person.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 我们没有给People类实现play方法,我们这里动态添加方法
if ([NSStringFromSelector(sel) isEqualToString:@"play"]) {
class_addMethod(self, sel, (IMP)myPlay, "v@:");
}
return [super resolveInstanceMethod:sel];
}
void myPlay(id self, SEL cmd)
{
NSLog(@"我要出去玩啦");
}
@end
在main.m中的代码如下:
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *aPerson = [[Person alloc] init];
[aPerson play];
}
return 0;
}
结果如下:
在Person.m文件里面,我们没有实现play
这个方法,所以如果调用了这个方法的话,就会调用+ (Bool)resolveInstanceMethod
,动态的添加方法.
Demo地址----->第一步的Demo里面的RuntimeMsg01
第二步:备选响应者
如果在上一步无法处理消息,那么Runtime会继续调用以下方法,Person.m中的关键代码如下:
//第一步:我们不动态添加方法,返回NO,进入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
//第二部:我们指定备选对象响应aStudent;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
Student *aStudent = [[Student alloc] init];
return aStudent;
}
在上面的代码中,我们指定了Student的一个实例对象aStudent来作为新的消息响应者. 在student.m中的关键代码如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 我们没有给Student声明和实现play方法,我们这里动态添加方法
if ([NSStringFromSelector(sel) isEqualToString:@"play"]) {
class_addMethod(self, sel, (IMP)myPlay, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void myPlay(id self, SEL cmd)
{
NSLog(@"小学生要开始玩啦");
}
forwardingTargetForSelector
如果返回了一个对象(不能是self自身,不然会出现死循环),那么这个对象就会作为消息的新接受者,并且消息会被分发到这个对象.
使用这个方法通常是在对象的内部,这样子在外部看起来,就好像由self(本身)处理了这个消息一样.
这一步适合我们指向将消息转发到另一个能处理该消息的对象上. 但这一步无法对消息进行处理,如操作消息的参数和返回值.
Demo地址---->第二步的Demo里面的RuntimeMsg02
第三步和第四步
如果第二步那里还不能处理未知消息,那么就会进入第三步. 代码如下:
// 第一步:我们不动态添加方法,返回NO,进入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
// 第二部:我们不指定备选对象响应aSelector,进入第三步;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
// 第三步:返回方法选择器,然后进入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"play"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 第四部:这步我们修改调用方法
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 在这里,dance的类型需要跟第三步中函数类型'v@:'相同
[anInvocation setSelector:@selector(dance)];
// 这还要指定是哪个对象的方法
[anInvocation invokeWithTarget:self];
}
// 若forwardInvocation没有实现,则会调用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"消息无法处理:%@", NSStringFromSelector(aSelector));
}
- (void)dance
{
NSLog(@"我要开始跳舞啦");
}
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息 有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在这个方法中选择将消息转发给其它对象,更改方法的实现等.
在这个方法中我们可以实现一些更加复杂的功能,我们可以对消息的内容进行修改,比如
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
修改NSInvocation对象里面的参数然后再去触发消息.
从某种意义上讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知消息转发给不同对象,或者执行不同的方法实现
Demo地址---->第三步的Demo里面的RuntimeMsg03
消息转发与多重继承
回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系, 以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种方法,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能 集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。总之,Objective-C通过这种方式,一定程度上减小了自己不支持多继承的劣势。
结尾
通过这几天的学习跟整理, 才写完这篇文章,在此,我们已经了解了Runtime中消息发送和转发的基本机制. 文笔的原因, 文章结构不是很清晰, 还请见谅。对运行时理解不到位,或者是有错误的地方,还请广大博友指出,感激不尽!
本文如果对您有帮助的话请随手给个喜欢哈,谢谢!
Demo地址---->全部的Demo
参考资料:
Objective-C Runtime 1小时入门教程
Objective-C Runtime 运行时之三:方法与消息