消息转发引入
instrumentObjcMessageSends方法的使用
在上一篇文章中我们介绍了通过方法决议的方式来处理未实现的方法崩溃问题;但显然这种一刀切的方式并不可取;那么除此之外,我们还有其他方法来处理么?
我们分析通过sel寻找imp的过程发现,最终还调用了log_and_fill_cache方法:
里边的logMessageSend方法会记录方法调用的日志流程,implementer是Class可以确定是存在的,那么只需要objcMsgLogEnabled的值为YES,那么就能够记录方法调用的日志信息了:
日志信息存储路径为/tmp/msgSends-%d,那么如果给objcMsgLogEnabled赋值为true呢?在源码中搜索objcMsgLogEnabled:
发现在方法instrumentObjcMessageSends中会给objcMsgLogEnabled赋值,那么通过调用此方法,就可以输入方法调用的日志信息了。
我们先来看一下没有实现的talk方法,在执行过程中的日志输出:
工程必须是个mac工程,比如
Command Line Tool工程
运行之后,最终生成日志文件:
我们打开日志文件,日志如下:
刚才我们打印的是对象方法的调用,接下来我们看一下类方法的调用:
生成文件如下:
可以看出类方法比对象方法多调用了resolveClassMethod,我们主要研究红框内的几个方法即可,其他的都是objc的底层调用的输出内容;resolveInstanceMethod和resolveClassMethod方法在之前的文章中我们已经介绍过了,也知道了其调用两次的原因,那么forwardingTargetForSelector,methodSignatureForSelector和doesNotRecognizeSelector是什么呢?这就是我们接下来要了解的消息转发流程
消息转发
快速转发 forwardingTargetForSelector
接下来,我们以对象方法的调用来分析消息转发的流程;在之前的文件中我们看到了了forwardingTargetForSelector方法的调用,那么这个方法是做什么的呢?
Returns the object to which unrecognized messages should first be directed.
大意是返回这个消息(unrecognized messages)应该首先指向的对象,也就是我们需要将这个无法识别的消息重定向给另一个对象,然后看这个对象能否调用;我们现在Teacher类中添加forwardingTargetForSelector的默认实现:
可以看到,虽然依然没有解决崩溃问题,但是forwardingTargetForSelector方法被成功调用,那么接下来在forwardingTargetForSelector中把方法重定向给其他对象呢?
新建一个Student类,在Student中实现talk方法,注意并不继承Person:
@interface Student : NSObject
@end
@implementation Student
- (void)talk {
NSLog(@"%s", __func__);
}
@end
然后在forwardingTargetForSelector中,将其重定向给Student类的对象:
可以看到调用结果:Student类中的方法talk被调用,而且Student与Person和Teacher并无任何继承上的联系;
这是一个快速转发的流程;
慢速转发 methodSignatureForSelector
那么如果Student也没有实现talk方法结果会如何呢?根据我们在日志文件中打印的方法执行流程可以知道,如果student也没有实现talk,那么必然会继续报错,执行methodSignatureForSelector方法,那么这个方法是什么意思呢?
Returns an NSMethodSignature object that contains a description of the method identified by a given selector.
Related Documentation
- forwardInvocation:
Overridden by subclasses to forward messages to other objects.
这个方法将会返回一个NSMethodSignature对象,其包含了被调用的方法的描述信息,也就是签名;并且,他需要与forwardInvocation:方法一起使用:
可以看到已经被methodSignatureForSelector拦截到,接下来补全forwardInvocation:方法的实现,forwardInvocation:用来解读签名,与methodSignatureForSelector一起使用:
v@:解读:
v表示void类型,表示talk的返回值@表示调用者类型self,即id类型:表示SEL
自始至终talk没有被实现,如果需要处理可以在forwardInvocation方法中获取到进行处理,否则将会方法将会流失掉;
hopper反汇编
接下来我们使用hopper工具来通过反汇编来分析消息转发;
运行原来的代码,在项目崩溃时,使用bt指令来查看堆栈信息:
在堆栈中明确看到之前调用日志中的方法doesNotRecognizeSelector,可以确定doesNotRecognizeSelector在CoreFoundation框架中,并且是___forwarding___和_CF_forwarding_prep_0触发的,那么我们尝试在CoreFoundation的源码中分析
可是,不管是forwarding还是forwarding_prep_0,在源码中都没有找到任何蛛丝马迹,看来源码分析无法走通,Apple并未开源这部分代码;
那么我们可以直接去看CoreFoundation的可执行文件进行反汇编分析:
CoreFoundation完整路径如下:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
接下来使用hopper来进行反汇编分析,将可执行文件使用hopper打开,选择x86(64 bits)架构:
从堆栈信息中我们可以明确是forwarding_prep_0触发了forwarding,那么我们以forwarding_prep_0为切入口进行分析:
我们以伪代码的方式来分析其流程,完整流程如下:
针对汇编分析,流程如下