runtime是什么
对于刚接触iOS或者初学者,runtime有点像十万个为什么,处处有疑惑。但作为一个iOS开发人员,runtime是深刻掌握Objective C所必须掌握的东西。它应该是OC最核心的部分,只有掌握好它,你才能理解其底层的原理和OC是个动态语言。要练成盖世神功,需练就自身深厚的内功,而runtime就是iOS开发的上层内功。
runtime是一个c和汇编写的动态库,他就像一个小小的系统,将OC和C紧密联系,它主要做两件事:
1、封装C语言的结构体和函数,让开发者在运行时创建、检查或者修改类、对象和方法等等。
2、传递消息,找出最终的执行代码。
为了让大家更好的理解runtime,举个例子 ---- 比如:给对象发送消息,如下面代码所示:
id returnValue = [someObject messageName:parameter];
在本例中,someObject叫做“接收者”(receiver),messageName叫做“选择子”(selector)。选择子与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数是消息传递机制中的核心函数,叫做objc_msgSend,其原型如下:
void objc_msgsend(id self,SEL cmd,...)
这是个“参数个数可变的函数”,能接受两个或者两个以上的参数。第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,其顺序不变。
除了上述提到的发送消息,runtime还提供了很多类似于它的方法,比如:
(BOOL)resolveInstanceMethod:(SEL)selector; //消息转发
objc_property_t * class_copyPropertyList(Class cls,unsigned int *ouCount); //获取属性列表
BOOL class_addMethod(class cls,SEL name,IMP imp,const char *types); //添加方法
void method_exchangeImplementations(Method m1,Method m2); //交换方法
runtime这么好,我们能利用它做什么事情呢?
1.遍历对象的属性 (JSON转模型)
2.动态添加方法,修改属性,替换方法 (swizzling 参考: http://blog.csdn.net/jasonjwl/article/details/50696424)
3.方法拦截调用 (给someObject发送messageName消息,但someObject不知道怎么messageName,那我们可以拦截,给该方法动态添加一个实现)
消息发送流程
以上面的例子进行说明
id returnValue = [someObject messageName:parameter];
1.编译器会把id returnValue=[someObject messageName:parameter]转化为id returnValue = objc_msgSend(someOject,@selector(messageName:),parameter);
2.objc_msgSend会把匹配结果缓存在“快速映射表”里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择相同的消息,执行起来会很快。
3.若是能找到与选择子名称相符的方法,就跳至其实现代码。
4.若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后在跳转。
5.如果最终还是找不到相符的方法,那就执行“消息转发”操作。
消息转发流程
1、动态方法解析
对象在收到无法解读(假设someObject的messageName方法尚未实现)的消息后,首先将调用其所属类的下列类方法:
+(BOOL)resolveClassMethod:(SEL)sel
该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,该方法与“resolveInstanceMethod:”类似,叫做"resolveClassMethod"。
使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用实现@dynamic属性,比如说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。
id autoDictionaryGetter(id self,SEL _cmd);
void autoDictionarySetter(id self,SEL _cmd,id value);
+(BOOL)resolveClassMethod:(SEL)selector{
NSString *selectorString = NSStringFromSelector(selector);
if (/* selector is from a @dynamic property */) {
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self,selector,(IMP)autoDictionarySetter,"v@:@");
}
else{
class_addMethod(self,selector,(IMP)autoDictionaryGetter,"@@:");
}
return YES;
}
return [super resolveInstanceMethod:selector]
}
首先将选择子化为字符串,然后检测其是否表示设置方法。若前缀为set,则表示设置方法,否则就是获取方法。
2、备援接受者
当前接受者还有第二次机会能处理位置的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。处理方法如下:
-(id)forwardingTargetForSelector:(SEL)aSelector
方法参数代表上文提到的未知的选择子(messageName),若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。通过此方案,我们可以用”组合“来模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自出来了这些消息似的。请注意,我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
3、完整的消息转发
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。
此对象包含选择子、目标与参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派到目标对象。此步骤会调用下列方法来转发消息:
-(void)forwardInvocation:(NSInvocation *)anInvocation
这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等等。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSOject类的方法,俺么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。
消息转发全流程
图1这张流程图描述了消息转发机制处理消息的各个步骤
图1 消息转发
接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运动期系统就可以将此方法缓存起来。
参考文献
《Effective Objective-c 2.0》编写高质量iOS与OS X代码的52个有效方法