前言
在之前的探索中,我们对整个消息发送的流程探索到了动态方法决议
这一步,在探索的过程中,我们遇到了一些问题,那就是resolveInstanceMethod
会调用两次
我们通过堆栈看到了两次调用之间的区别
- 第一次调用的路径是
main
->_objc_msgSend_uncached
->lookUpImpOrForward
->resolveInstanceMethod
。也就是系统在快速方法查找
和慢速方法查找
之后,如果没有找到方法的IMP
那么就会给我们一次补救的机会,调用resolveInstanceMethod
方法。 - 第二次调用的路径是
main
->_CF_forwarding_prep_0
->___forwarding___
->methodSignatureForSelector
->class_getInstanceMethod
->lookUpImpOrForward
->resolveInstanceMethod
.其中_CF_forwarding_prep_0
是系统库CoreFundation
中的内容,它发起了第二次的resolveInstanceMethod
,由于CoreFundation
库并没有开源,接下来我们通过反汇编的形式,来探索之后的流程。
CoreFundation库的反汇编
准备工作
首先我们新建个iOS
的工程,然后再main
中打上断点,通过模拟器运行起来。
在断点的时候输入命令iamge list
查看当前加载的库列表,找到CoreFoundation
库的路径,然后将这个可执行文件使用Hopper
反编译工具打开。
forwarding方法
我们在之前探索的过程中了解到,如果动态方法决议依旧没有处理方法的话,那么系统会调用__forwading__
方法,而这个方法在CoreFoundation
库中,接下来我们就来看看这个方法,在反编译的模式下,能看出点什么东西
可以看到,在伪代码
的模式下,我们能够比较友好的来了解__forwarding__
方法的功能。一部分一部分来看
int ____forwarding___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
// ……省略部分初始化相关……
loc_115fb4:
var_150 = r12;
var_140 = rdx;
var_148 = r15;
//**从这里可以知道,rbx实际上是实例对象,rax就是我们的目标类**
rax = object_getClass(rbx);
r15 = rax;
r12 = class_getName(rax);
//**r15 = rax,r15就是目标类,这里就是判断类中是否有forwardingTargetForSelector方法**
//**如果没有则跳去loc_116064标记行,否则继续往下执行**
//这个流程我们称之为,快速消息转发
if (class_respondsToSelector(r15, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_116064;
loc_115ff5:
//**到这说明已经实现了forwardingTargetForSelector方法,直接调用它**
rax = [rbx forwardingTargetForSelector:var_140];
//**如果返回为0x0或者rax==rbx,即调用过后依旧没有找到方法的IMP**
//**则直接跳去loc_116064标记行(与未实现方法跳转的地方一致)**
if ((rax == 0x0) || (rax == rbx)) goto loc_116064;
loc_116012:
//**如果返回不为0x0,即已经找到了方法的IMP,则跳去loc_11604a行**
if (rax >= 0x0) goto loc_11604a;
在这段伪代码里,我们可以很明显的看到系统调用了一个叫做forwardingTargetForSelector
的方法,也就是快速消息转发流程
,如果成功匹配到IMP
则去loc_11604a
位置,否则去loc_116064
位置,那么我们来看看他们分别都是什么
loc_11604a:
//**var_148 = r15 也就是目标类**
//**var_138是偏移量,这里应该是将找到的IMP插入到对应sel的缓存中**
*(var_148 + var_138) = rax;
r15 = 0x0;
goto loc_11638e;
loc_11638e:
//**用来检查进出函数时堆栈是否平衡和数据是否被修改**
if (**___stack_chk_guard == **___stack_chk_guard) {
rax = r15;
}
else {
rax = __stack_chk_fail();
}
return rax;
这一段代码意思实际上就是,将找到的IMP
匹配到SEL
上,表示方法匹配成功。那么我们再看看loc_116064
的代码
loc_116064:
//**rbx就是实例对象,放到var_138暂存**
var_138 = rbx;
if (strncmp(r12, "_NSZombie_", 0xa) == 0x0) goto loc_1163cd;
loc_116087:
//**r14 = 实例对象,r15 = 类对象**
r14 = var_138;
//**类对象是否响应methodSignatureForSelector方法**
//**所以通过上面代码我们知道这是当forwardingTargetForSelector没匹配方法成功时调用**
//**我们把这里成为慢速消息转发**
if (class_respondsToSelector(r15, @selector(methodSignatureForSelector:)) == 0x0) goto loc_1163e3;
loc_1160a8:
//**这里的rbx又等于SEL了**
rbx = var_140;
//**调用methodSignatureForSelector方法**
rax = [r14 methodSignatureForSelector:rbx];
//**返回值为0x0,则去loc_11645e位置**
if (rax == 0x0) goto loc_11645e;
loc_1160c7:
r15 = rax;
rax = [rax _frameDescriptor];
r12 = rax;
if (((*(int16_t *)(*rax + 0x22) & 0xffff) >> 0x6 & 0x1) != r13) {
rax = sel_getName(rbx);
rcx = "";
if ((*(int16_t *)(*r12 + 0x22) & 0xffff & 0x40) == 0x0) {
rcx = " not";
}
r8 = "";
if (r13 == 0x0) {
r8 = " not";
}
_CFLog(0x4, @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", rax, rcx, r8, r9, var_150);
}
//**_forwardStackInvocation带下划线的基本都是系统内部函数**
if (class_respondsToSelector(object_getClass(r14), @selector(_forwardStackInvocation:)) == 0x0) goto loc_1161fe;
loc_1161fe:
var_150 = r12;
r12 = r14;
if (class_respondsToSelector(object_getClass(r14), @selector(forwardInvocation:)) == 0x0) goto loc_11642b;
loc_11622a:
//**准备forwardInvocation方法需要的参数**
rax = [NSInvocation _invocationWithMethodSignature:r15 frame:var_148];
r13 = rax;
//**调用forwardInvocation方法**
[r12 forwardInvocation:rax];
var_140 = 0x0;
rbx = 0x0;
r12 = var_150;
goto loc_11626b;
loc_11642b:
r14 = @selector(forwardInvocation:);
____forwarding___.cold.4(&var_130, r12);
rcx = r14;
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_138, rcx, r8, r9, var_150);
goto loc_116457;
这块就是当我们的快速消息转发流程
依旧没有匹配到相应的IMP
后,系统再给的一次机会,即慢速消息转发流程
,而methodSignatureForSelector
通常与forwardInvocation
方法一起使用,组成完整的慢速消息转发流程
。继续看如果慢速消息转发
还没成功匹配,那么接下来系统会怎么做
loc_116457:
rbx = var_140;
goto loc_11645e;
loc_11645e:
rax = sel_getName(rbx);
r14 = rax;
rax = sel_getUid(rax);
if (rax != rbx) {
rcx = r14;
r8 = rax;
_CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_140, rcx, r8, r9, var_150);
}
if (class_respondsToSelector(object_getClass(var_138), @selector(doesNotRecognizeSelector:)) == 0x0) goto loc_1164d1;
loc_1164b8:
(*_objc_msgSend)(var_138, @selector(doesNotRecognizeSelector:));
asm { ud2 };
rax = loc_1164d1(rdi, rsi, rdx, rcx, r8, r9);
return rax;
这里很明显,就是调用doesNotRecognizeSelector
方法,报错没有找到对应的方法,然后进入崩溃了。到这里我们已经整体的梳理了下快速消息转发
和慢速消息转发的
的流程,但是还是没有解决我们一开始的问题:为什么会调用两次resolveInstanceMethod
方法。我们再来看看第二次调用resolveInstanceMethod
方法时的堆栈信息
我们可以看到,在调用methodSignatureForSelector
后,调用了NSObject
的methodSignatureForSelector
方法,那么我们去源码中看看这个方法的实现
苹果的注释告诉我们,这个方法的实现实际上是在CoreFundation
库中,那么继续去反编译中查看该方法
void * +[NSObject instanceMethodSignatureForSelector:](void * self, void * _cmd, void * arg2) {
rdx = arg2;
rdi = self;
//**如果调用方self != nil 并且___methodDescriptionForSelector返回不为空**
//**则调用默认实现,否则返回空**
if ((rdx != 0x0) && (___methodDescriptionForSelector(rdi, rdx) != 0x0)) {
rax = [NSMethodSignature signatureWithObjCTypes:rdx];
}
else {
rax = 0x0;
}
return rax;
}
这里在返回系统返回默认值之前,有个判断___methodDescriptionForSelector
方法,而我们在堆栈中也看到了这个方法,那就继续看看他
int ___methodDescriptionForSelector(int arg0, int arg1) {
// ……省略……
loc_7c5d4:
r15 = *(rax + r13 * 0x8);
rdi = r12;
r14 = r12;
r12 = rax;
rax = class_isMetaClass(rdi);
rax = protocol_getMethodDescription(r15, rbx, 0x1, rax == 0x0 ? 0x1 : 0x0);
if (rax != 0x0) goto loc_7c64e;
loc_7c602:
rax = class_isMetaClass(r14);
rax = protocol_getMethodDescription(*(r12 + r13 * 0x8), rbx, 0x0, rax == 0x0 ? 0x1 : 0x0);
r12 = r14;
if (rax != 0x0) goto loc_7c65c;
// ……中间省略……
loc_7c673:
rax = class_getSuperclass(r12);
r12 = rax;
rbx = var_50;
if (rax != 0x0) goto loc_7c5b4;
loc_7c68b:
rax = class_getInstanceMethod(var_40, rbx);
if (rax != 0x0) {
rax = method_getDescription(rax);
r15 = *rax;
rbx = *(rax + 0x8);
}
else {
rbx = 0x0;
r15 = 0x0;
}
goto loc_7c6b2;
// ……省略……
}
其他的东西我们就不去研究了,但是我们在其中看到了一个我们很熟悉的方法class_getInstanceMethod
没错,就是我们的慢速方法查找流程
中的关键一环,而后会调用lookUpImpOrForward
最后再次进入resolveInstanceMethod
方法。打印两次的谜底在这里就揭开了。
反汇编总结
通过反汇编系统的CoreFundation
库,我们大致的了解到了系统在动态消息决议
之后做了什么
- 快速消息转发流程
首先调用
forwardingTargetForSelector
方法,再给我们一次机会,快速的指定是否有其他的对象能够处理其他的方法。 - 慢速消息转发流程
慢速消息转发流程由两个方法组成
methodSignatureForSelector
与forwardInvocation
。这是系统给我们的最后一次机会,来动态的添加方法的实现。 - 异常报错
当最后两次机会还没有抓住的话,系统就会进行异常报错,调用
doesNotRecognizeSelector
方法,最后崩溃。
快速消息转发流程
快速消息转发就是通过调用forwardingTargetForSelector
方法,让我们能够快速的指定一个其他的对象,如果它实现了目标方法,那么也会成功的调用。我们举个例子🌰
@interface DMWorker : NSObject
- (void)sayBye;
@end
@implementation DMWorker
- (void)sayBye {
NSLog(@"%s",__func__);
}
@end
@interface DMPerson : NSObject
- (void)sayBye;
@end
@implementation DMPerson
//快速消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"%s",__func__);
if (aSelector == @selector(sayBye)) {
return [DMWorker alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
DMPerson *p = [[DMPerson alloc] init];
[p sayBye];
}
return 0;
}
非常的简单,在DMWoker
类中实现sayBye
方法,在DMPerson
快速方法查找中指定DMWorker
响应sayBye
方法,然后我们看看打印。
DMPerson
调用sayBye
方法,DMWorker
响应,很完美。但是这种方式调用的方法名和参数必须完全一样,比较死板,接下来就看看我们的慢速消息转发吧
慢速消息转发流程
相比快速转发流程
,慢速转发流程
给与我们的自由度相对来说会大很多,我们继续来看个🌰
@interface DMPerson : NSObject
- (void)sayBye;
- (void)eat:(SEL)selector;
@end
@implementation DMPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"%s",__func__);
return [super resolveInstanceMethod:sel];
}
//快速消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"%s",__func__);
return [super forwardingTargetForSelector:aSelector];
}
//**慢速消息转发**
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s",__func__);
if (aSelector == @selector(sayBye)) {
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@::"];
return signature;
}
return [super methodSignatureForSelector:aSelector];
}
//**这个方法里面是空实现也不会崩溃,但是没有转发的功能**
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"%s",__func__);
SEL originSel = anInvocation.selector;
if (originSel == @selector(sayBye)) {
anInvocation.target = self;
anInvocation.selector = @selector(eat:);
//**0位置是targe 1位置是sel,所以自定义的参数传递只能从2开始**
[anInvocation setArgument:&originSel atIndex:2];
[anInvocation invoke];
}
}
- (void)eat:(SEL)selector{
NSLog(@"%s: %@",__func__,NSStringFromSelector(selector));
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
DMPerson *p = [[DMPerson alloc] init];
[p sayBye];
}
return 0;
}
看看日志输出
成功的通过sayBye
方法,调用了eat
方法的实现。
当我们在methodSignatureForSelector
中指定响应的NSMethodSignature
后,那么方法的调用就不会崩溃,而在forwardInvocation
方法中,是指定具体的响应方法。在forwardInvocation
中我们可以将自己想要的任何方法、任何参数都给定制化。
总结
系统在通过快速方法查找
和慢速方法查找
确定我们需要调用的方法没有实现以后,一共给我们三次机会去纠正这个错误
- 动态方法决议
resolveInstanceMethod
- 快速消息转发
forwardingTargetForSelector
- 慢速消息转发
methodSignatureForSelector
+forwardInvocation
我们可以通过下面这个图来梳理一下他们的关系