runtime的消息机制
前面提到过编译器最终会把我们的消息发送转化为函数调用
- 消息发送 [object sendMassage]
首先编译器会在运行时将上面的例子转化为objc_msgSend(obj,@selector(sendMassage))这个函数,转换的时候除了方法本身的参数之外,还有两个隐藏的参数一个是id类型的,代表对象的类型,还是一个是SEL类型的,是函数对应的方法的编号,接下来就会按照下面的流程来调用这个方法
- 通过obj的isa指针找到其所对应的类。
- 通过SEL先去类的cache列表中找这个方法,如果就去找方法的实现,不存在,进入第3步
- 去类的method列表中找,如果就去找方法的实现,没有找到,根据类中的superclass指针去父类中找,一直到NSObject.
找到了方法之后就要去找方法的实现,那么如何找方法的实现呢,runtime提供了两种方式
IMP class_getMethodImplementation(Class cls, SEL name);
IMP method_getImplementation(Method m)
在id objc_msgSend(id self, SEL _cmd, ...) {
Class class = object_getClass(self);
IMP imp = class_getMethodImplementation(class, _cmd);
return imp ? imp(self, _cmd, ...) : 0;
}
可以看到这个方法中的第二行代码imp,可以通过这个imp来查找这个方法的实现,要是没有找到,runtime给我们提供了三次机会让我们的程序不会崩溃,也就是下面要提到的动态方法解析和消息转发(消息重定向,消息转发)
* 动态方法解析
resolveInstanceMethod或者resolveClassMethod给你提供一次添加方法实现的机会
下面例子,在student类中有一个没有实现的write方法,但是没有实现,如果我们掉用它,会因为找不到实现程序崩溃,有下面的挽救措施
//在这个c语言函数添加方法的实现
void test(){
NSLog(@"我是动态添加的方法");
}
//然后实现这个方法,当这个方法返回no时或者没有实现时会进入消息转发
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(write)) {
class_addMethod([self class], sel, (IMP)test, "[email protected]:");
}
return [super resolveInstanceMethod:sel];
}
如你添加了函数实现并且返回了yes,那么就会调用。不会进入下面的消息转发,否则进入下面的消息转发
- 消息转发
消息转发分为两步,一次是消息的重定向,返回一个实现了该方法的对象,还有一次是真正的转发,是把这个函数的参数及相关信息打包成一个对象,把他发送给另外一个类,让他去处理(PS:消息重定向是返回一个提供了实现的对象,消息转发是将方法参数打包到一个对象里面,然后把这个对象发送出去)。
- 消息重定向:需要实现这个函数- (id)forwardingTargetForSelector:(SEL)aSelector,通过这个函数返回一个实现了该方法的对象。如果返回了self或者no的时候,就会进入消息转发,否则不会
定义一个新类,里面添加一个和student中未实现的方法同名的方法,而且这个方法有实现,假设叫testStudent,然后在student中实现下面的方法
//该方法返回一个添加了方法实现的对象,
-(id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(write)) {
//studentTest类中这个方法有实现
StudentTest *test = [[StudentTest alloc]init];
return test;
}
return [super forwardingTargetForSelector:aSelector];
}
- 消息转发:需要实现两个方法
methodSignatureForSelector:返回一个NSInvocation*的对象,将方法的参数返回值封装在里面。
- (void)forwardInvocation:(NSInvocation *)anInvocation,将该对象发送给一个提供了方法实现的对象。
和上面的消息重定向一样,只不过student中要实现的方法有了差别
//这个方法返回一个NSInvocation*的对象,里面打包了有关于这个未实现方法的信息
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString*sel = NSStringFromSelector(aSelector);
if ([sel isEqualToString:@"write"]) {
return [NSMethodSignature signatureWithObjCTypes:"[email protected]:"];
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
SEL selector = [anInvocation selector];
StudentTest *test = [[StudentTest alloc]init];
if ([test respondsToSelector:selector]) {
[anInvocation invokeWithTarget:test];
}
}
总结一下
发送一个消息:底部会转成objc_msgSend函数,这个函数除了方法的参数之外还有两个隐藏的参数self和_cmd,接下来就会按照下面的流程去调用这个函数
- 根据isa指针找到对象所属的类或者类所属的元类
- 先去类或者元类的cache列表中根据SEL去找这个方法。
- 没有找到,去method方法列表中找
- 还是没有找到,就去父类中找
- 找到了,根据SEL找到对应的IMP,调用这个函数
- 没有找到,进入动态方法解析或者消息转发。
动态方法解析:
runtime提供的第一次实现这个方法的机会,要实现resolveInstanceMethod/resolveClassMethod方法,给未实现的方法在运行时添加实现,返回no/不实现,进入消息转发。
消息转发一:消息重定向
消息重定向:返回一个实现了该方法的对象,要实现-(id)forwardingTargetForSelector:(SEL)aSelector函数,如果返会nil/self,则进入下面的消息转发二
消息转发二:消息转发
将未实现方法的相关信息打包成一个NSInvocation对象,然后交给一个类去实现。需要实现-(NSMethodSignature )methodSignatureForSelector:(SEL)aSelector和-(void)forwardInvocation:(NSInvocation )anInvocation方法
疑问?为什不直接找IMP而要通过SEL这个中间人呢?
SEL只是方法的编号,真是的实现是通过IMP来查找的,SEL和IMP之间是一一映射的关系,通过SEL我们可以改变他的IMP,然后让一个方法在不同的情况下有不同的实现,例如实现方法的交换,有时候我们需要给系统的方法添加一些自己的东西
1:可以通过一个子类继承于系统类,然后重写那个类的方法
2:通过分类,但是会覆盖系统的方法
3:写一个自己的方法,通过runtime在load方法中交换系统方法和自己的方法的实现
下面主要针对第三种例子举例:
第一步首先为我们要动手脚的系统方法类添加一个分类,
假设我们要为imageNamed添加一个判断nil的功能,先要为他添加一个分类,然后给系统的imageNamed方法添加前缀,明明一个自己的方法,如下
分类的h文件
分类的m文件,对于这里乍一看可能像递归,其实在第一次调用的系统的imageNamed方法时调用的是my_imageNamed方法,当第调用的my_imageNamed方法时其实在调用系统的imageNamed方法
交换
至于为什么要在load方法中写,我会在别的博客中提到。