上一篇文章初步讲了一下objc_msgSend
的作用,这里是传送门初步理解objc_msgSend
。那么,对象在收到消息之后无法通过objc_msgSend
发送的消息之后会怎么办呢?
由于OC是动态语言,所以在运行时还可以继续向类中添加方法,所以当对象收到无法解读的消息时,就会启动消息转发
机制,我们可以经由这个机制告诉程序该怎么处理这种消息。
消息转发会分成两大阶段,第一阶段叫做动态方法解析(dynamic method resolution):先征询当前接收者所属的类,是否能动态添加方法并处理这个未知的selector,如果接收者没有动态添加方法或者动态添加的方法依然不能处理这个未知的selector,则当前接收者自己就没有办法通过动态新增方法的手段来响应这个selector了,之后就进入消息转发的第二阶段。第二阶段可以分成两步,第一步接收者会查看是否存在其他对象能处理这条消息,如果有,则这个处理消息的对象叫备援接收者(replacement receiver),runtime系统会把消息转发给这个对象,消息转发流程结束。第二步,如果连备援接收者都没有,则启动完整的消息转发,runtime系统会把和消息有关的所有信息都放进NSInvocation对象中,再给接收者一次机会,处理未知的selector,如果这一步都失败了,就会抛出unrecognize selector send to instance xxx这个异常。
看完上面这段文字,再结合下面这张消息转发流程图,应该就能对整个消息转发流程有个比较形象的认知了:
下面具体看看每一步的过程。
动态方法解析
对象在收到无法解读的消息后,会先调用所属类的一个类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector;
该方法的参数就是objc_msgSend
无法处理的selector,返回的布尔值表示这个类能否新增一个实例方法处理它。如果这个seletor不是一个实例方法而是一个类方法,那么会有个类似的类方法调用:
+ (BOOL)resolveClassMethod:(SEL)selector;
在这个阶段处理未知selector的前提是相关代码已经提前写好了,只等着运行时动态插入类中就可以了。
这个方案经常用来实现@dynamic属性
,coreData中的NSManagerObject中的属性就是这么做的。由于CoreData的属性需要从数据库中读取,然后进行动态绑定,而不是通过自动生成的setter和getter去实现。其属性getter和setter实现方法在编译期就已经写好,等到动态方法解析的时候进行setter和getter的方法添加。
备援接收者
在这一步中,runtime系统会提供一个方法,让当前接收者返回一个备援接收者来处理未知的selector,这个方法如下:
- (id)forwardingTargetForSelector:(SEL)selector;
如果当前接收者能找到或者提供这样一个对象,就将其返回,不能就返回nil。
我们可以利用这一部,来模拟多重继承
的一些特性。比如在一个对象内部,还有其他很多对象,这个对象可以经由这个方法,选择一个处理selector的对象并返回。在外部看来,好像对象是亲自处理该对象的一样。
也可以利用这个过程去完成一些很棒的设计模式,比如装饰器
模式。具体的例子,会在后续文章更新中慢慢补上。
这里要注意的是,在这个过程中,是无法操作经由这一步转发的消息的。
完整的消息转发
当前两步都宣告失败之后,runtime系统会创建一个NSInvocation对象,把尚未处理的消息与有关的细节信息全部封装到里面,这个对象包含selector,target以及参数。这个步骤会调用以下方法转发消息:
- (void)forwardInvation:(NSInvocation*)invocation;
实现这个方法很简单,只要改变目标,让消息在新目标中调用即可。也可以通过改变参数、更换selector等,变得应用场景更加多变。
这个方法实现时,如果发现不应该由本来调用forwardInvation
方法,就需要调用它父类的同名方法,这样,继承体系中所有的类都有机会处理调用请求,一直到NSObject。如果最后调用了NSObject类的方法,最后会以doesNotRocgnizeSelector
的方式抛出异常。
上面说过,在第二步备援接收者中是无法处理消息的。而在完整的消息转发
中不仅能够操作消息,还能轻松拿到消息相关的所有信息。所以,一些看似黑魔法的实现实际上就在完整的消息转发
这个过程中实现的,比如JSPatch
和Aspects
这两个开源库关键的步骤就是在这个过程中完成的。