消息转发机制
前言
书接上回方法慢速查找过程分析,我们知道,在lookUpImpOrForward
中如果找到了方法实现imp
,那么就会走到log_and_fill_cache
去缓存方法,未找到方法imp,会进入resolveMethod_locked
动态方法决议,如果动态方法决议
中仍未处理的话,后面系统会将消息进行转发,下面我们来看看消息转发
的流程。
1.消息转发入口
首先跟之前慢速查找流程一样,先看看消息转发的入口在哪里?我们先看看log_and_fill_cache
的源码:
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill(cls, sel, imp, receiver);
}
注意,有这个宏SUPPORT_MESSAGE_LOGGING
-->满足条件的话会打印日志logMessageSend
,源码如下:
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
上面源码中,通过对objcMsgLogFD
值的判断,来处理日志文件目录的创建secure_open
,以及后面日志的写入write (objcMsgLogFD, buf, strlen(buf));
,看来这个objcMsgLogFD
很关键,我们再来全局搜下给它赋值的地方,如下图:

发现红框处是赋值的地方,在方法instrumentObjcMessageSends
里面,源码如下
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
根据源码大致可以看出,这个方法是用来控制logMessageSend
日志记录的,那么我们可以借助该方法,查查在动态方法决议
后消息仍未处理的情况下,后面系统将调用哪些方法?
那么如何使用呢?因为这个方法是在.mm实现文件里,无法被外边调用,此时需要使用关键字extern
extern void instrumentObjcMessageSends(BOOL flag);
再查看logMessageSend
里打印日志的路径在哪里?
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
根据上面代码,在/tmp/
路径下,生成的msgSends-
名称开头的文件。
示例验证--打印消息调用日志记录
首先创建新工程千万别用源码的工程
,创建类LGPerson
@interface LGPerson : NSObject
- (void)sayHello;
@end
//----------------分割线------------------
@implementation LGPerson
@end
只声明sayHello
方法,但不实现该方法。
在main入口中,调用该方法如下:
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *s = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[s sayHello];
instrumentObjcMessageSends(NO);
}
return 0;
}
运行代码,打开Finder,前往/tmp/
路径查看,多了个msgSends文件

打开文件,在动态方法决议
resolveInstanceMethod :
后执行的是forwardingTargetForSelector:
和methodSignatureForSelector:
,找到了,这就是消息转发
的入口!
2.消息转发具体流程
上面我们找到了消息转发的入口方法,先后调用了forwardingTargetForSelector:
和methodSignatureForSelector:
,分别看看这两个方法的具体含义。哪里看?-->当然是苹果爸爸的官方文档搜索!
2.1 forwardingTargetForSelector
首先查看官方文档说明

方法描述大致是:会优先调用这个方法,返回一个对象,去处理这个未识别的消息。返回类型为
id对象类型
,参数是未处理的消息SEL
。
那么我们用代码验证下:
@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s -- %@", __func__, NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
@end
运行工程,控制台输出如下:

虽然最终报错,但是报错前确实进入了
[LGPerson forwardingTargetForSelector:] -- sayHello
,说明系统确实调用了forwardingTargetForSelector
方法。
我们再修改代码,让其完成处理消息转发:
先声明一个类LGStudent
@interface LGStudent : LGPerson
@end
//----------------分割线------------------
@implementation LGStudent
- (void)sayHello {
NSLog(@"%s", __func__);
}
@end
//----------------调用处------------------
#import "LGStudent.h"
@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s -- %@", __func__, NSStringFromSelector(aSelector));
return [LGStudent alloc];
}
@end
//----------------分割线------------------
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
[p sayHello];
NSLog(@"Hello, World!");
}
return 0;
}
控制台输出:
2020-09-25 16:22:33.339387+0800 003-cache_t脱离源码环境分析[11476:2659115] -[LGPerson forwardingTargetForSelector:] -- sayHello
2020-09-25 16:22:33.339755+0800 003-cache_t脱离源码环境分析[11476:2659115] -[LGStudent sayHello]
2020-09-25 16:22:33.339797+0800 003-cache_t脱离源码环境分析[11476:2659115] Hello, World!
Program ended with exit code: 0
消息成功发送给了类LGStudent
处理,没有报错!在forwardingTargetForSelector
这一步就完成了消息转发,后面不会调用methodSignatureForSelector
,这也说明了forwardingTargetForSelector
会优先被系统调用。这就是所谓的快速转发
流程!
2.2 慢速转发
剩下的方法是methodSignatureForSelector
,既然有快速转发
,对应的肯定有慢速转发
,那么它就是慢速转发
。还是先看官方文档说明
2.2.1 事务处理
forwardingTargetForSelector 快速转发
methodSignatureForSelector 慢速转发,查看官方说明文档,发现与NSInvocation搭配使用-(void)forwardInvocation

方法描述大致是:返回一个
NSMethodSignature
方法签名对象,去处理这个未识别的消息。
再看下类NSMethodSignature
@interface NSMethodSignature : NSObject
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
@property (readonly) NSUInteger numberOfArguments;
- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx NS_RETURNS_INNER_POINTER;
@property (readonly) NSUInteger frameLength;
- (BOOL)isOneway;
@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;
@property (readonly) NSUInteger methodReturnLength;
@end
- 有一个类方法
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
可构造NSMethodSignature
对象。 - 根据索引获取参数的类型
- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx NS_RETURNS_INNER_POINTER;
-
- (BOOL)isOneway;
一个判断方法,不知道判断啥,暂不考虑 - 一些属性,暂不考虑
看来我们只有通过这个类构造方法去创建方法签名对象`NSMethodSignature,还是查查这个方法的官方文档说明

参数是一个method arguments
列表,再在官网文档搜索下关键词method arguments

红框处点击查看详情:

终于找到了方法参数的说明表格:

接着我们试着代码验证:
@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s -- %@", __func__, NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s -- %@", __func__, NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
@end
运行看看:

还是报错了,按照文档说明,既然处理了这一步,应该不会报错才对,再回去看看methodSignatureForSelector
的文档,拉到最下面

需配合这个方法使用,再看看forwardInvocation

交给了一个
NSInvocation
对象去处理。再改下代码:
@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s -- %@", __func__, NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s -- %@", __func__, NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s -- %@", __func__, anInvocation);
}
@end
运行看看:

成功了!
我们再仔细看看
NSInvocation
:
有两个和方法关联的属性:
target 和 selector
,那么我们明白了,它能指定消息处理的targe接收者和 selector方法指针,接着我们代码再次验证:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s -- %@", __func__, anInvocation);
anInvocation.target = [LGStudent alloc];
anInvocation.selector = @selector(sayHello);
[anInvocation invoke];
}

完美解决,大功告成!
总结
综上所述,我们通过日志打印,发现了消息转发的入口方法,通过官网文档,一步一步用代码验证消息的转发流程,最终验证成功!现在总结一下消息转发的大致流程,流程图如下:
