前言
我们知道OC是一门动态语言、它提供了一个RunTime库把代码中的类型检测、方法调用等一系列操作放到了运行期。这固然对语言的灵活性来说是一极大的优势,但这也给我们开发带来了一个让人头疼的问题 --> 《unrecognized selector sent to instance 0x1c400f420》;相信我们在开发中都遇到过这种的Crash提示。
下面列举几个导致这种Crash Log的示例代码。
示例1、
示例2、
正如我们开篇所叙述的那样在编译和链接期、上面两个代码没有任何问题、但是在运行期就会出现 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ACTestForwardObject length]: unrecognized selector sent to instance 0x1c0202fc0'这样的Crash Log。
那么我们如何避免上面的这些问题呢?在Crash之前OC到底做了什么?我们是否可以在Crash之前对这个错误进行补救呢?
答案是肯定的、OC在 unrecognized selector 之前其实已经为我们做了很多次努力(具体来说是三次)来让我们有足够的机会来挽回APP出现Crash,而这就是我们要说的消息转发机制。
一、什么是消息转发(Message Forward)
我们知道OC对象调用方法其实是调用底层的objc_msgSend()函数,而方法查找的大致过程是通过遍历当前类->父类->根类的方法列表来查找是否有对应的方法(这里对方法调用不做过多叙述)。如果在查找过程中找到对应的方法实现,则进行方法调用;如果直到根类依然没有找到对应的方法实现、那么接下来便是消息转发 --> Show Time!!!!
如果直到根类都没有遇到传说中的方法实现,那么OC将触发消息转发机制。而所谓的消息转发大概过程如下图:
从图中我们可以看到如果在继承关系的方法列表中没有找到Method,将进行下面的三步去处理不能识别的方法。
<1>、通过resolveInstanceMethod:我们可以动态的为类添加方法实现。
<2>、通过forwardingTargetForSelector:我们可以返回一个可以处理aSelector的对象。
<3>、通过methodSignatureForSelector:(方法签名)和forwardInvocation:(封装方法到Invocation)做最后的挣扎。
如果上面三步对aSelector做了处理,则程序正常执行。否者程序最后将调用doesNotRecognizeSelector:方法输出前言中的Crash Log。
二、消息转发代码实现
首先我们实现两个类:ACTestForwardObject和ACTestInvicationObject。
1.ACTestInvicationObject实现
ACTestInvicationObject主要作为上面消息转发中的Target返回,所以该类只实现一个方法如下图:
2. ACTestForwardObject实现
ACTestForwardObject.h中我们声明一个logClassMethod函数用于输出对象的方法列表内容、如下图。
依次在ACTestForwardObject.m中重写或实现消息转发相关的函数、如下图:
消息转发的第一步:
消息转发的第二步:
消息转发的第三步:
该函数主要返回方法签名,如果返回Nil,forwardInvocation将不会调用、直接调用doesNotRecognizeSelector 导致Crash。
这里对anInvocation对象做处理,最后invoke Invocation。
重写doesNotRecognizeSelector进行Log输出。
其他私有函数实现:
第三步Invocation被替换的方法实现:
第一步动态添加的方法实现:
用于输出方法列表的函数:
三、程序调试验证
我们在程序启动的时候调用如下代码:
验证一:
按照我们截图的代码运行程序输出Log如下:(注意这里第二步和第三步中我取了非,实际没有进入判断条件)根据Log我们再回看图P1-1-1消息转发过程是不是更清楚了☺.
验证二:
在验证一条件的基础上,将P-2-3中的isResolve条件取反,并放开注释掉的 IMPdefaultMethod =(IMP)acDefaultRecognizedSelector; class_addMethod(self.class, sel, defaultMethod,"v@:");重新运行程序输出Log如下:
验证三:
在验证一条件的基础上,将图P-2-4中的[targetObjectrespondsToSelector:@selector(length)]前面的取非去掉,重新运行程序,输出Log如下:
验证四:
在验证一条件的基础上,将图P-2-6中的isEqual前面的取反去掉,重新运行程序,输出Log如下:
验证五:
在验证一条件的基础上,我们将图P-2-5中的[method isEqualToString:@"length"]取反;并将图P-2-6中的isEqual前面的取反去掉,重新运行程序。输出Log如下:
四、总结
通过上面的1、2、3、4验证及Log输出,我们可以相信OC中的消息转发分为三个步骤:(1)、resolveInstanceMethod,(2)、forwardingTargetForSelector,(3)methodSignatureForSelector + forwardInvocation;这三个步骤是按照顺序依次调用的、如果步骤1对aSelector进行了处理、那么之后的2、3将不在执行;依次类推如果三步都没有对aSelector做处理那么将调用doesNotRecognizeSelector之后输出系统Log程序Crash。通过验证5我们可以推断出第三步中methodSignatureForSelector是必要条件。如果methodSignatureForSelector返回Nil,则forwardInvocation将不会被调用。