在前面两章中介绍了方法消息的处理流程,宏观上来说,方法的本质就是对消息的发送,处理消息的过程呢,我们经历了objc_msgSend快速查找、慢速查找。在前面两个环节中,依然在本类、父类继承链、元类继承缓存中找未找到消息,又未采取动态方法决议,对未查找到的方法实现resolveInstancMethod
,则就会报错奔溃。这样对于开发者来说是不愿看到的,所以对消息的处理就来到了新的层次,进行消息转发,本章内容将围绕这个展开。
铺垫
通过前面分析lookUpImpOrForward
,既然是寻找或者转发,那在没寻找到的情况下,它是怎么转发的呢?入口又是在哪?
- 通过
instrumentObjcMessageSends
分析方法调用顺序
换一个思路,既然动态决议后,如果没有对Imp进行操作,就会崩溃,那可以通过该方法检测奔溃时方法的调用情况。lookUpImpOrForward -> log_and_fill_cache -> logMessageSend
,objcMsgLogEnabled =YES
是进入这个流程的关键.
//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);
}
//-------------------------------------------------
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
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;
}
// 1: objcMsgLogEnabled 控制开关
// 2: extern
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;
}
// SUPPORT_MESSAGE_LOGGING
#endif
/tmp/
这个路径就是就是将奔溃日志输出到本地临时缓存了,
在main.m中调用Book的burnBook的未实现方法,extern
:这是一个关键字,是告诉编译器在编译时不要报错,在该类中不存在的方法,请去别的类查找。
使得objcMsgLogEnabled =true
,则就可以查看到在本地生成的一个文件。调用依然会奔溃,但log会输出的。
其中
forwardingTargetForSelector
就是快速转发方法;慢速转发则是methodSignatureForSelector
,其实还搭配forwardInvocation
使用。
消息转发-快速转发
我们该怎么使用forwardingTargetForSelector
呢?这个时候可以瞄一瞄苹果文档
如果有无法识别的消息,就将其转发到指定的对象。那我们就可以进行一个操作,再定义一个English的类。如果识别到burnBook被调用,我们就将其转发给另外一个类的方法中,English类中实现了这个方法,就会在English中找寻这个方法。
// English.m
-(void)burnBook
{
NSLog(@"English burn book");
}
Book中将方法转发出去,把目标对象返回。
#import "Book.h"
#import "English.h"
@implementation Book
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"burnBook"]) {
return [English alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
转发到English
,English
实现了该burnBook
方法,最终会打印出结果,也不会报错。
这样我们就可以在开发过程中很好的利用这样一点,结合动态方法决议,在需要的地方添加方法,避免应用奔溃,或者利用运行时动态执行一些自定义方法都是很好的方向。
消息转发-慢速转发
在快速转发消息之后,就会来到慢速转发(标准转发)消息。找到运行时的方法methodSignatureForSelector
。
返回方法的签名,在苹果的文档中解释了方法的使用场景,也指出在转发消息时要创建
NSInvocation
对象。我们先验证下log里面方法调用顺序,依然是调用
burnBook
,但是不在forwardingTargetForSelector
中处理。再实现方法签名方法,返回父类方法,程序继续奔溃,但是也表示我们的方法按照log中的顺序走下来了。现在,我们对其进行签名,并实现
forwardInvocation
:经过慢速转发,程序已经不再奔溃,它已经将消息转发出去,自己也不再处理。
方法签名图
我们可以查看下NSInvocation
结构
我们可以验证下签名之后的
anInvocation
中有哪些东西表明签名后的信息都传递到了
- (void)forwardInvocation:(NSInvocation *)anInvocation
我们可以在将其消息转发给
English
消息转发流程
我们发现和我们之前分析的log文件中方法调用顺序不一致,那么我们可以再做一个操作,实现
resolveInstanceMethod
,打印结果,我们就能发现,与msgSends-10393
中的方法执行顺序的一致
经过一系列的转发,我们可以大致总结到以下一个流程