前言
在上一篇底层原理之动态方法决议我们说消息发送先经过objc_msgSend快速查询IMP,查询不到IMP就进入lookUpImpOrForward慢速查询,如果慢速查询再查询不到,苹果爸爸贴心的给我们一次机会,叫动态方法决议,只要在动态方法决议中实现了IMP,就可以防止方法找不到的崩溃。那么动态方法决议中没有实现IMP的话,苹果爸爸还会不会再给个机会呢?oc源码。
msgSend日志
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
cls->cache.insert(sel, imp, receiver);
}
分析:我们在探索消息的慢速查询时,如果查询到了IMP会调用log_and_fill_cache插入缓存。如果objcMsgLogEnabled==true且implementer存在,就会调用logMessageSend方法插入日志,源码搜索logMessageSend如下:
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
return false;
}
分析:当进行消息发送的时候,会创建一个/tmp/msgSends-%d的日志记录,那么如果想要查看日志objcMsgLogEnabled必须为true。全局搜索一下objcMsgLogEnabled发现instrumentObjcMessageSends()函数控制了objcMsgLogEnabled的值
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
if (objcMsgLogEnabled == enable)
return;
if (enable)
_objc_flush_caches(Nil);
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
分析:所以想要消息发送输出日志,可以向该函数传true,想要停止输出日志可以传false。
写个demo试一下:新建LGPerson类,初始化并调用不存在的方法sayHello()
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
分析:在private/tmp文件夹下果然发现一个msgSends-8209文件。打开如下
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject methodSignatureForSelector:
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject doesNotRecognizeSelector:
.....
分析:调用resolveInstanceMethod动态方法解析之后,系统先会调用forwardingTargetForSelector,然后再调用methodSignatureForSelector。这就是我们今天探索的主题消息转发。
forwardingTargetForSelector快速转发
源码全局搜索该函数发现只有方法声明没有实现,应该是在cf框架里不开源
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
xode:command+shift+0 搜索一下该函数的定义
# forwardingTargetForSelector:
Returns the object to which unrecognized messages should first be directed.
分析:forwardingTargetForSelector含义是返回未识别消息重定向的对象,简单理解指定一个对象,让这个对象去接收这个消息。
接着上面的LGPerson实例,我们再写一个GyTest类实现sayHello()方法,通过forwardingTargetForSelector转发到GyTest看是否可以。
@implementation LGPerson
-(id)forwardingTargetForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqual:@"sayHello"]){
return [[GyTest alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}
输出:
**---[GyTest sayHello]---**
分析:通过forwardingTargetForSelector把消息转发给类GYTest,让其实现SEL的IMP果然是可以的。注意sayHello()是对象方法,如果调用的是类方法,就要通过+(id)forwardingTargetForSelector把消息转发出去。GYTest收到消息会重新进入消息查找流程,快速、慢速、动态方法决议、消息转发。
methodSignatureForSelector慢速转发
源码搜索该函数同样有两个没有源码的类方法和对象方法。xode:command+shift+0 搜索一下该函数的定义
# methodSignatureForSelector:
Returns an `NSMethodSignature` object that contains a description of the method identified by a given selector.
....
....
[`- forwardInvocation:`]()
Overridden by subclasses to forward messages to other objects.
分析:methodSignatureForSelector的含义是返回一个NSMethodSignature对象。methodSignatureForSelector一般搭配和forwardInvocation使用,如果methodSignatureForSelector方法返回的是一个nil就不会调用forwardInvocation
接着上面的LGPerson实例,注释掉快速转发
@implementation LGPerson
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqual:@"sayHello"]){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"进入慢速转发:%@---%@",anInvocation.target,NSStringFromSelector(anInvocation.selector));
if( [NSStringFromSelector(anInvocation.selector) isEqual:@"sayHello"]){
GyTest* test=[[GyTest alloc]init];
anInvocation.target=test;
//如果想实现GyTest其他方法的话
// anInvocation.selector=@selector(anyotherway);
[anInvocation invoke];
}
}
输出:
进入慢速转发:<LGPerson: 0x10075df90>---sayHello
---[GyTest sayHello]---
分析:
methodSignatureForSelector返回NSMethodSignature实例,那么就会进入forwardInvocation方法,如果返回为nil,那么就不会进入forwardInvocation方法。只要进入了forwardInvocation方法就可以防止方法找不到的奔溃,不管有没有实现消息转发。- forwardInvocation中的
NSInvocation对象保存着消息接收者和SEL,可以重定向消息接收者和SEL,调用invoke实现消息的转发。
补充
消息发送的流程已经分析结束了,补充一张流程图帮助记忆和理解
总结:
-
快速转发:forwardingTargetForSelector实现对象的重定向,进入指定对象的方法查找流程,如果返回是nil,进入慢速转发流程 -
慢速转发:methodSignatureForSelector返回值是nil,慢速查找流程结束。如果有返回值就进入forwardInvocation,只要进入此方法就可以避免方法找不到的奔溃,可以重定向消息接收者和SEL,调用invoke实现消息的转发。