前言:
iOS进阶之传递消息 上篇讲到消息传递,其中有个问题:对象在收到无法解读的消息之后会发生什么情况? 若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经此过程告诉对象应该如何处理位置消息。
上面这段异常信息是由NSObject的“doesNotRecognizeSelector:”方法所抛出的,此异常表明:消息接受者的类型是__NSCFNumber,而该接受者无法理解名为lowercaseString的选择子。本例所列举的这种情况并不奇怪,因为NSNumber类里本来就么有名为lowercaseString的方法。控制台的中看到的那个__NSCFNumber是为了实现“无缝桥接”而使用的内部类,配置NSNumber对象时也会一并创建此对象。在本例中,消息转发过程以应用程序崩溃而告终,不过,开发者在编写自己的类时,可于转发过程中设置挂钩,用以执行预定的逻辑,而不使应用程序崩溃。 消息转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。若没有“备援的接收者”,则启动完整的消息的转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
动态方法解析
对象在收到无法解读的消息后,首先将调用其所类的下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实类用于处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。加入尚未实现的方法不是实例方法而是类方法,那么运行期就会调用另外一个方法,该方与“resolveInstanceMethod”类似,叫做“resolveClassMethod:”。 使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方法常用来实现@dynamic属性,比如说,要访问CoreData框架中NSManagedObjects对象时就可以这么做,因为实现这些属性所需的存储方法在编译器就能确定。
备援接受者
当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,则将其返回,若找不到,就返回nil。通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。 请注意,我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
完整的消息转发
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标及参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。 此步骤会调用下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation
这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接受者”方案所实现的方法等效,所以很少有人采用这么简单的实现方法。比较有用的实现方式为:在触发消息前,先以某种改变消息内容,比如追加一个参数,或是改变选择子,等等。 实现此方法时,若发现某调用操作不应由本类处理,则需要调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。
消息转发全流程
接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能够在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。若想在第三步里把消息转给备援的接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二步执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。
要点
1️⃣若对象无法响应某个选择子,则进入消息转发流程。
2️⃣通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
3️⃣对象可以把其无法解读的某些选择子转交给其他对象来处理。
4️⃣经过上述两步后,如果还是没法处理选择子,那就启动完整的消息转发机制。