Objective-C Runtime 解读 (二)
关于runtime的解读, 前一章节主要讲解了基本的概念, 其实runtime的运用是无处不在的, 本章节主要解读runtime在”消息转发机制”中的体现.
消息发送的理解
其实, 我们一般所说的函数调用, 在OC中我们更习惯叫做消息发送, 一般会这样写: [someObj dosomething], 这种[ ]的写法, 其实就是消息发送, 其实最终是转换成了下面的形式:
objc_msgSend(someObj,@selector(dosomething));
有参数的是这样:
objc_msgSend(someObj,@selector(dosomething:),var1);
看到这里, 其实我们就已经很清楚, 为什么说OC是基于消息发送的机制, 其实底层也就是调用了C函数.
消息发送机制
可能我们经常遇到下面的崩溃信息:
2016-04-01 11:41:03.867 RunTimeTestDemo[19405:1760968] -[ViewController clickAction:]: unrecognized selector sent to instance 0x7fff33c27600
2016-04-01 11:41:03.874 RunTimeTestDemo[19405:1760968] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController clickAction:]: unrecognized selector sent to instance 0x7fff33c27600'
出现这种情况就是, 我们发送的消息没有被正确解析出来, 那么, 接下来就详细介绍OC中的动态消息转发机制.
其实消息的转发可以大致分为两个阶段, 一个是动态方法解析, 而是完整的消息转发, 那么接下来先看第一个阶段:
动态方法解析
在这里, 就不得不提到OC的强大之处, 因为OC是动态语言, 因此消息的解析是运行时完成的, 还记得前一章节提到的isa指针, 那么动态方法解析的时候就需要使用到isa指针了.
首先消息发送之后, 先查询接受者, 也就是通过isa指针查找相对应的method_list分发表, 如果没有查找到指定的selector, 那么会接着延superClass继续查找, 直到查到根类, 如果这个过程中该选择子可以被执行了, 那么动态解析也就结束了, 如果不可以, 那么接下来就看当前类能否动态添加方法, 来处理这个未知的选择子了.
对象收到无法解读的消息之后, 会调用所属类的下列方法:
//实力方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
//类方法
+ (BOOL)resolveClassMethod:(SEL)sel
在这里其实我们还是可以处理这个未知的选择子, 通过动态添加方法的形式, 处理未知的选择子, 这也是第一个阶段, 到最后runtime机制给我们提供的处理未知选择子的方法.
大致可以这样解决:
void addMethod(id obj, SEL _cmd)
{
NSLog(@"Doing add method");
NSLog(@"%@", obj);
NSLog(@"%@", NSStringFromSelector(_cmd));
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(clickAction:)) {
class_addMethod([self class],sel,(IMP)addMethod,"v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
通过动态添加方法的形式来处理这个未知的选择子, 到这里第一阶段的工作也就基本结束了, 在第一阶段中, 其实我们已经可以处理未知的选择子了, 当然如果没有在第一阶段处理掉, 其实runtime还是给了我们第二次处理的机会, 接下来就说说完整的消息转发阶段:
完整的消息转发
其实,这个阶段也可以分为两小节来看待, 首先就是实现备援接受者, 这个其实跟前一个阶段的实现形式差不多, 大致实现如下:
- (id)forwardingTargetForSelector:(SEL)aSelector{
TargetObject *object = [TargetObject new];
if (aSelector == @selector(clickAction:) && [object respondsToSelector:aSelector]) {
return object;
}
return [super forwardingTargetForSelector:aSelector];
}
这种处理方式, 其实更像向备援接受者”借用”了这个方法, 其实仔细想想, 这个跟继承的概念有点像, 但又不一样, 因为当前类没有继承备援类, 但是却可以使用备援类中的方法, 而继承也是可以这样的, 如果当前类有多个备援接受者, 其实就可以近似看成”多继承”类理解.
还有一点需要注意的是, 如果当前类借用了备援接受者中的很多方法来处理未知的选择子, 那么其实就可以直接继承来实现了, 因为通过动态消息解析的方式是比较消耗资源的, 因此, 建议这种情况下, 直接继承实现.
当着一部分还无法处理是, 接下来就需要启动完整的消息转发机制了, 此步骤回调用下列方法转发:
- (void)forwardInvocation:(NSInvocation *)anInvocation
这一阶段首先会创建Invocation对象, 将于未处理选择子相关的信息装在里面, 包括: 选择子, 目标以及参数等, 大致解决方式如下:
//首先需要their method signatures
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
TargetObject *object = [TargetObject new];
return [object methodSignatureForSelector:aSelector];
}
//然后, 重定向
-(void)forwardInvocation:(NSInvocation *)invocation
{
SEL invSEL = invocation.selector;
if([altObject respondsToSelector:invSEL]) {
[invocation invokeWithTarget:altObject];
} else {
[self doesNotRecognizeSelector:invSEL];
}
}
这个方法其实比较简单, 只是改变了调用目标, 是的消息在新目标上执行, 这其实跟备援接受者的实现方式类似, 都是讲未知的选择子转给可以处理的对象来处理.
下面这张图描述了消息转发机制处理消息的每个步骤:
该图选自Effective Objective-C 2.0 这本书, 建议大家阅读一下, 里面有很多高效开发的知识点, 还有一些很深入的讲解.
上图中可以看出, 在整个消息转发机制中, 每个阶段都可以处理未知的选择子, 但是步骤越往后, 处理消息的代价就越大, 最好是能够在第一步就处理掉, 这样的话, 其实代价相对较小.
总结
本文主要讲解了OC中动态消息转发的各个阶段, 也主要体现了runtime机制的强大之处, 通过运行期的动态方法解析, 我们可以动态添加需要的方法到类中, 也可以使用备援接受者来处理, 最后还可以通过完整的消息转发机制, 来处理未知消息.
因此, runtime机制使得我们的程序更健壮, 而且更灵活, 可以再运行期做更多的事情.
参考:
Objective-C Runtime Programming Guide
理解 Objective-C Runtime