一、msgSend消息发送监听
在探索了很多次了lookUpImpOrForward方法中,查找完成后会写入cache
在写入cache中发现有个打印log的操作
我们来看下是否可以通过这个输出到本地的日志,是否可以查看到一些不一样的流程和我们还没探索过的方法。
上图中看到如果想要调用logMessageSend方法,需要满足判断条件:objcMsgLogEnabled && implementer,implementer是我们传入的class,那么影响判断的就只有objcMsgLogEnabled这一个条件了。
objcMsgLogEnabled默认是flase,全局搜索一下发现赋值在instrumentObjcMessageSends方法里。
那我们调用下试试,是否可以打印出日志。
由于源码工程跑不了这个方法,我们新建一个工程来跑。
发现还是报错,正常,那么我们在电脑里全局搜索一下是否有msgSend-xxx文件。
打开看一下:
这些方法没有探索过,我们逐个探索。
二、消息快速转发
Xcode中shift+command+0,打开文档,看下forwardingTargetForSelector的官方解释。
- (id)forwardingTargetForSelector:(SEL)aSelector;
作用:返回无法识别的消息应首先指向的对象。
讨论:如果一个对象实现(或继承)此方法,并返回一个非nil(和非self)结果,则该返回的对象将用作新的接收方对象,消息分派将继续到该新对象。(显然,如果从这个方法返回self,代码将陷入无限循环。)
如果你在非根类中实现这个方法,如果你的类对于给定的选择器没有返回任何东西,那么你应该返回调用super的实现的结果。
该方法使对象有机会在昂贵得多的forwardInvocation:机制接管之前,重定向发送给它的未知消息。当您只想将消息重定向到另一个对象,并且比常规转发快一个数量级时,这很有用。如果转发的目标是捕获NSInvocation,或者在转发期间操作参数或返回值,那么它就没有用了。
相当于重定向。来试一下效果。
@interface LGPerson : NSObject
- (void)sayInstanceMethod;
@end
@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s---%@", __func__, NSStringFromSelector(aSelector));
return [LGStudent alloc];
}
@end
@interface LGStudent : NSObject
- (void)sayInstanceMethod;
@end
@implementation LGStudent
- (void)sayInstanceMethod{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayInstanceMethod];
}
return 0;
}
输出:
-[LGPerson forwardingTargetForSelector:]---sayInstanceMethod
-[LGStudent sayInstanceMethod]
成功转发到LGStudent,并调用。
三、 消息慢速转发
如果我不实现forwardingTargetForSelector:呢?再看下一个日志调用方法methodSignatureForSelector:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
methodSignatureForSelector:
作用:返回一个NSMethodSignature对象,该对象包含由给定选择器标识的方法的描述。
如果LGStudent中没实现目标实例方法呢?
打印输出:
-[LGPerson forwardingTargetForSelector:]---sayInstanceMethod
-[LGStudent sayInstanceMethod]: unrecognized selector sent to instance 0x103805450
发现还报错,而且并没有走methodSignatureForSelector方法,因为调用forwardingTargetForSelector方法的时候就已经重定向到LGStudent里了,就不会继续走LGPerson类里的流程了。我们把forwardingTargetForSelector方法注释掉。
注释以后发现还会报错!
查看methodSignatureForSelector的官方文档后发现,需要和另一个方法搭配使用 ,才能实现消息转发。
- (void)forwardInvocation:(NSInvocation *)anInvocation;
作用:被子类重写以将消息转发到其他对象。
重要:为了响应对象本身不能识别的方法,除了forwardInvocation:之外,还必须覆盖方法signatureforselector:。转发消息的机制使用从methodSignatureForSelector:获得的信息来创建要转发的NSInvocation对象。您的覆盖方法必须为给定的选择器提供适当的方法签名,方法是预先确定一个或向另一个对象请求一个。
实现一下:
发现还是报错,头疼~
其实是因为我们没有对methodSignatureForSelector签名做处理,即返回值。
这里的返回值中的type是一个签名 ,【iOS底层】05:类原理分析(下)有讲到。
打印:
-[LGPerson methodSignatureForSelector:]---sayInstanceMethod
-[LGPerson forwardInvocation:]---<NSInvocation: 0x104019f50>
看出这两个方法都执行了,也没有crash了,但是我们的sayInstanceMethod并没有执行~而且此时已将LGStudent中的sayInstanceMethod方法实现的注释放开了。
出现此问题的原因在于:经过cache查找、methodlist慢速查找,消息快速转发之后均未找到对应imp,那么系统认为是找不到了,那么又不能让程序crash,那么就hold住了,但是什么也不做。
如果我们需要继续走下去呢?
就涉及到在forwardInvocation方法里继续操作了~
可以看到anInvocation里存着receiver和imp方法,以待开发者再进行处理,默默等候。
四、forwardInvocation继续探索
既然forwardInvocation静候开发者操作,那么我们可以进行怎么操作呢?
我们还可以这样处理,转发到LGStudent,调起它的sayInstanceMethod方法~
五、反汇编CoreFoundation
上边的操作似乎有点上帝视角,那么我们不做哪些操作,在程序crash之后再lldb中bt查看崩溃前的堆栈,发现了我们上边没探索的doseNotRecognizeSelector方法,它是在CoreFoundation库里的,但不幸的是苹果并没有对这个库开源,文档中也没有寻找到有用的信息,那么我们有什么办法来探索这个方法呢?
分析CoreFoundation可执行文件!
这个通过编译程序lldb输入image list,查找到CoreFoundation的地址,然后在磁盘里路径下找~
这里就要用到一个工具软件:Hooper
打开后是介个亚子滴~
- 先来看___forwarding___:
这里要用这种模式查看。
双击红框代码进入。
往下找就找到了forwardingTargetForSelector:如果未响应,则跳转loc_64a67
如果methodSignatureForSelector:实现了继续往下走到forwardInvocation:
如果methodSignatureForSelector:没找到就执行了doesNotRecognizeSelector:
至此也印证了我们再msgSends-xxx本地日志文件记录的流程。
六、IDA反汇编
我们在windows上电脑上安装一个IDA,将CoreFoundation文件拖入IDA软件。
双击这里以后按F5进入Pseudocode-A伪代码模式。
也可以看到相应的流程方法~小伙伴们有时间可以深入去探索~
七、动态决议方法为什么走两次
上篇文章我们遗留了一个问题,为什么打印的时候resolveInstanceMethod:方法调用了两次。
resolveInstanceMethod里打断点,第二次的时候lldb输入bt查看堆栈。
我们在resolveInstanceMethod打个断点看看。
第一次,很好理解,正常的消息发送未找到对应的imp,执行__objc_msgSend_uncached,进入消息转发流程lookUpImpOrForward。
第二次,出现了我们见过的_CF_forwarding_prep_0、___forwarding___.
methodSignatureForSelector中调用了__methodDescriptionForSelector,__methodDescriptionForSelector调用了class_getInstanceMethod方法,最终又调到了lookUpImpOrForward。这样才走了第二次~
总结、
消息转发流程图:
先欠着~
至此消息转发流程我们已经探索完了,期待下一篇文章~