运行时
网上对运行时机制有很多笼统的说法,但相信还是会有很多人并不完全理解运行时的机制。那么什么是运行时呢?
先来看一种写法:
@interface MyClass : NSObject
{
@public
NSString *_myName;
@private
NSString *_myID;
}
OC中是支持public和private关键字的,类似于java或C#,但是我们在编写OC代码的时候却很少这么做。因为使用这种写法,对象布局在编译器就已经固定了。只要碰到访问_myName变量的代码,编译器就把其替换为偏移量(offset),这个偏移量是硬编码,表示该变量距离存放对象的内存区域的起始地址有多远。但是如果在运行过程中,又新增了一个实例变量,硬编码于其中的变量就会读到错误的值,例如我们假设每个变量的指针都是4个字节,新增一个变量myGender:
@interface MyClass : NSObject
{
@public
NSString *_MyGender;(offset为0)
NSString *_myName;(offset为+4)
@private
NSString *_myID;(offset为+8)
}
如果offset采用硬编码,原类中offset为0是应该访问到_myName现在却访问到_myGender。
OC的做法是,把实例变量当做一种存储offset所用的特殊变量,交由类对象保管。offset会在运行期查找,如果类的定义变了,那么offset也会相应改变,这样的话,无论何时访问实例变量,总能访问到正确的偏移位置。
这就是我们所说的运行时机制。想更深入的了解运行时机制可以看看这篇文章:
http://quotation.github.io/objc/2015/05/21/objc-runtime-ivar-access.html
这篇文章同时解释了为什么OC无法动态添加成员变量。
消息转发
再来说一说消息转发,消息转发是运行时机制的一大特点。在了解消息转发之前先来了解一个OC的底层函数:
void objc_msgSend(id self, SEL cmd, ...)
在OC中,所有的方法最终都会转换为普通的C语言函数,例如一个对象object的方法:
- (id)doSomething:(id)parameter
{
//doSomething
}
给object发送消息:
id returnValue = [object doSomething:patameter];
object是接受者,doSomething叫做选择子,选择子与参数合起来叫做消息,编译器看到此消息后,会将其转换为一条标准的C语言函数,就是最开始的那个函数,它是消息传递机制中的核心函数,上述消息会转换为:
id returnValue = objc_msgSend(object,@selector(doSomething:),parameter);
objc_msgSend函数会根据接受者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接受者所属的类中搜寻方法列表,如果找到了名称相符的方法,就跳转至其实现代码,如果找不到,就沿着继承体系向上查找。所过最终依然没有找到相符的方法,就会执行消息转发。
一个完整的消息转发过程会经历三个阶段:
- 动态方法解析(resolveInstanceMethod或resolveClassMethod)
- 备选接收者(forwardingTargetForSelecor)
- 完整消息转发(forwardInvocation)
</br>
1. 动态方法解析
在消息转发开始时,本类有机会新增一个处理选择子的方法,如果选择子是实例方法会调用:
+ (BOOL)resolveInstanceMethod : (SEL)selector
如果是类方法,则会调用:
+ (BOOL)resolveClassMethod : (SEL)selector
例如,用此方案来实现@dynamic属性:
id autoDictionaryGetter(id self, SEL _cmd);
id autoDictionarySetter(id self, SEL _cmd, id value);
+ (BOOL)resolveInstanceMethod:(SEL)selector
{
NSString *selectorString = NSStringFromSelector(selector);
if (/*选择子是个@dynamic属性*/)
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,就是set方法,否则是get方法。不管哪种情况,都会把处理该选择子的方法动态的加到类里面,以最在开始处理消息转发。例子中用到了IMP指针,想了解IMP指针可以看看这篇博客:
//www.greatytc.com/p/425a39d43d16?utm_campaign=maleskine&utm_content=note&utm_medium=writer_share&utm_source=weibo
2. 备选接收者
如果在动态方法解析时没有处理消息转发,那么还有第二次机会来处理未知的选择子,在这一步中系统会询问:有没有其他接收者来处理这条消息?该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
但是我们无法操作经由这一步所转发的消息,如果想在发送给备选接受者之前先修改消息内容,就得通过完整的消息转发机制来做了。
3. 完整的消息转发
首先,创建NSInvacation对象,把与尚未处理的那条消息有关的全部细节都封装在其中,此对象包括选择子,target及参数。在触发NSInvocation对象时,消息转发系统将亲自出马,把消息指派给目标对象。
此过程会调用:
- (void)forwardInvocation:(NSInvocation *)invocation
这个方法可以实现的很简单:只要改变调用target,使消息在新targer上得以调用即可。然而这样的实现就与备选接受者的实现方法等效了。一般的做法是:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数,或更改选择子等等。
实现此方法时,若发现调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最终此消息未得到处理,则会调用NSObject的doesNotRecognizeSelector:
,以抛出异常。
完整的消息转发用到NSInvocation对象,想了解NSInvocation可以看一看这篇博文:
http://mp.weixin.qq.com/s?__biz=MjM5NTIyNTUyMQ==&mid=208927760&idx=1&sn=30b9caecba709553e463d719668454ae&scene=2&from=timeline&isappinstalled=0#rd
4. 完整的消息转发流程图
另外需要注意的是,消息转发过程中,步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统可以将此方法缓存。如果这个类的实例还会再接收到同名选择子,那么根本无须再次启动消息转发流程。
另外一篇介绍运行时机制的博文:
http://www.cocoachina.com/ios/20150715/12540.html