前言
有关Runtime的知识总结,我本来想集中写成一篇文章的,但是最后发现实在是太长,而且不利于阅读,最后分成了如下几篇:
OC方法的调用其实是消息的发送,
消息的发送其实是C语言函数的调用
在Runtime中不得不提的就是OC的消息处理和消息转发机制。我们知道在OC中的实例对象调用一个方法称作消息传递,OC中里的消息传递采用动态绑定机制来决定具体调用哪个方法,OC的实例方法在转写为C语言后实际就是一个函数,但是OC并不是在编译期决定调用哪个函数,而是在运行期决定。
OC究竟是怎么将实例方法转换为C语言的函数,又是如何调用这些函数的呢?这些都依靠强大的runtime。
在深入代码之前介绍一个clang编译器的命令:
clang -rewrite-objc main.m
该命令可以将.m的OC文件转写为.cpp文件
有如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
//为了方便查看转写后的C语言代码,将alloc和init分两步完成
Person *p = [Person alloc];
p = [p init];
p.name = @"Jiaming Chen";
[p showMyself];
}
return 0;
}
通过上述clang命令可以转写代码,然后找到如下定义:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("init"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1);
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("showMyself"));
}
return 0;
}
OC语句被 clang编译器转化为 C语言后的样子。
当你调用一个类的方法时:
(1)先在本类中的方法缓存列表中进行查询,如果在缓存列表中找到了该方法的实现,就执行,
(2)如果找不到就在本类中的方列表中进行查找。在本类方列表中查找到相应的方法实现后就进行调用,
(3)如果没找到,就去父类中进行查找。如果在父类中的方法列表中找到了相应方法的实现,那么就执行,
否则就执行消息处理与消息转发相关的方法。
总结一下流程图就是如下:
这里要特别说明下:
A:在这个resolveInstanceMethod 方法中,这个函数是给类利用class_addMethod添加函数的机会。根据文档,如果实现了添加函数代码则返回YES,未实现返回NO。如果没有添加函数代码就算返回YES,也无任何意义,还是会往下走。
B:返回的这个对象,如果也无法响应这个 SEL就会跟当前这个情形一样,会调用这个对象的resolveInstanceMethod 方法,如果未作处理也会帮抛出异常。
C: methodSignatureForSelector:(SEL)aSelector 这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行。
forwardInvocation: 在这个函数里可以将NSInvocation多次转发到多个对象中,这也是这种方式灵活的地方。(forwardingTargetForSelector只能以Selector的形式转向一个对象)
下面具体介绍下相关方法的使用:
一、消息处理(Resolve Method)
首先,如果沿继承树没有搜索到相关方法则会向接收者所属的类进行一次请求,看是否能够动态的添加一个方法。
当在相应的类以及父类中找不到类方法实现时会执行+resolveInstanceMethod:这个类方法。该方法如果在类中不被重写的话,默认返回NO。
在该方法中,我们可以为找不到实现的SEL动态的添加一个方法实现,添加完毕后,就会执行我们添加的方法实现。这样,当一个类调用不存在的方法时,就不会崩溃了。
在Model的父类、BaseViewController的父类中实现如下方法,
可以避免调用没有实现的方法造成的崩溃。
#import "MyObject.h"
#import "NSObject+NoMethod.h"
@implementation MyObject
/** 没有找到SEL时会执行下面的方法 */
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
[self addMethod:sel methodImp:@selector(dynamicAddMethod)];
return YES;
}
- (void)dynamicAddMethod
{
NSLog(@"找不到方法时执行这里");
}
@end
二、消息单项转发
如果不对上述消息进行处理的话,也就是+resolveInstanceMethod:返回NO时,会走下一步消息转发,即-forwardingTargetForSelector:
。
该方法会返回一个类的对象,这个类的对象有SEL对应的实现,当调用这个找不到的方法时,就会被转发到SecondClass中去进行处理。这也就是所谓的消息转发。当该方法返回self或者nil, 说明不对相应的方法进行转发,那么就该走下一步了。
- (id)forwardingTargetForSelector: (SEL)aSelector
{
// return [[Test alloc]init];
return nil;
// return self;
})
三、消息多项转发
如果不将消息转发给其他类的对象,那么就只能自己进行处理了、或者崩溃。会用到如下两个方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
(1) -methodSignatureForSelector:
方法来获取方法的参数以及返回数据类型,也就是说该方法获取的是方法的签名并返回。
注意:如果
methodSignatureForSelector
返回的NSMethodSignature 是 nil 的话不会继续执行forwardInvocation
,转发流程终止,抛出无法处理的异常。
methodSignatureForSelector如何实现?
methodSignatureForSelector用于描述被转发的消息,描述的格式要遵循以下规则点击打开链接
首先,先要了解的是,每个方法都有self和_cmd两个默认的隐藏参数,self即接收消息的对象本身,_cmd即是selector选择器,所以,描述的大概格式是:返回值@:参数。@即为self,:对应_cmd(selector).返回值和参数根据不同函数定义做具体调整。
比如下面这个函数
-(void)testMethod;
返回值为void,没有参数,按照上面的表格中的符号说明,再结合上面提到的概念,这个函数的描述即为 v@:
v代表void,@代表self(self就是个对象,所以用@),:代表_cmd(selector)
如果实在拿不准,不会写,还可以简单写段代码,借助method_getTypeEncoding方法去查看某个函数的描述,比如
-(NSString *)testMethod2:(NSString *)str;
描述为 @@:@
-(void)testMethod
{
Method method = class_getInstanceMethod(self.class, @selector(testMethod));
const char *des = method_getTypeEncoding(method);
NSString *desStr = [NSString stringWithCString:des encoding:NSUTF8StringEncoding];
NSLog(@"-> %@",desStr);
}
-(NSString *)testMethod2:(NSString *)str
{
Method method = class_getInstanceMethod(self.class, @selector(testMethod2:));
const char *des = method_getTypeEncoding(method);
NSString *desStr = [NSString stringWithCString:des encoding:NSUTF8StringEncoding];
NSLog(@"-> %@",desStr);
return @"";
}
把数字去掉,剩下v@: ,与之前我们的描述一致。
结果是@@:@ ,与之前结论一致。
(2) 这个时候runtime
会将未知消息的所有细节都封装为NSInvocation
对象,然后调用下述方法:
- (void)forwardInvocation: (NSInvocation*)invocation;
在这个函数里可以将NSInvocation多次转发到多个对象中。
调用这个方法如果不能处理就会调用父类的相关方法,一直到NSObject
的这个方法,如果NSObject
都无法处理就会调用doesNotRecognizeSelector:
方法抛出异常。
综合运用如下:
@implementation Test
-(void)func3;
{
NSLog(@"KK: %s",__func__);
}
@end
@implementation Test2
-(void)func3;
{
NSLog(@"KK: %s",__func__);
}
@end
@implementation Person
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if(aSelector == @selector(func3))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
NSMethodSignature *new= [super methodSignatureForSelector:aSelector];
return new;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if (anInvocation.selector == @selector(func3))
{
Test *h1 = [[Test alloc] init];
Test2 *h2 = [[Test2 alloc] init];
[anInvocation invokeWithTarget:h1];
[anInvocation invokeWithTarget:h2];
}
}
[person func3];
person中没有实现func3方法,但最终运行后,程序没有报错,且Test 和 Test2 的func3 方法都被执行了。
了解更多 NSInvocation
An NSInvocation is an Objective-C message rendered static, that is, it is an action turned into an object. NSInvocation objects are used to store and forward messages between objects and between applications, primarily by NSTimer objects and the distributed objects system.
NSInvocation有点类似于java里的反射,它有一套完整的装备:target,selector,returnValue,ArgumentArray,有了它们,NSInvocation就可以动态的invoke任意对象的任意方法了。