从objc4
官方的源码上看,当一个方法经过快速查找和慢速查找后,会经过动态方法决议进行第一次补救,然而苹果仅仅给我们这一次补救机会吗?从源码上我们无法获取更多信息,但我们可以通过其他方式获得方法信息。
通过log打印方法调用信息
我们来到lookUpImpOrForward
函数,在done
的执行流程中,会调用到log_and_fill_cache
方法,跟进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);
}
跟进logMessageSend
,查看是如何打印消息信息的。
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;
}
可以看到,生成的日志回写入到/tmp
路径下的文件中去,然而写入文件需要objcMsgLogEnabled
为true的条件下才能开启,而objcMsgLogEnabled
默认是false,所以我们需要设置这个参数为true。
而开启这个开关需要调用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;
}
现在已经找到开关函数,但是这是objc源码的函数,我们可以使用extern关键字将这个函数暴露出来。
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
instrumentObjcMessageSends(YES);
[[Animal alloc] eat];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
执行之后可以看到/tmp路径下多了一个文件
[图片上传失败...(image-6a4fae-1602225141126)]
执行cat /private/tmp/msgSends-94170
➜ Deserve git:(master) cat /private/tmp/msgSends-94170
+ Animal NSObject initialize
+ Animal NSObject alloc
+ Animal NSObject resolveInstanceMethod:
+ Animal NSObject resolveInstanceMethod:
- Animal NSObject forwardingTargetForSelector:
- Animal NSObject forwardingTargetForSelector:
- Animal NSObject methodSignatureForSelector:
- Animal NSObject methodSignatureForSelector:
- Animal NSObject class
+ Animal NSObject resolveInstanceMethod:
+ Animal NSObject resolveInstanceMethod:
- Animal NSObject doesNotRecognizeSelector:
- Animal NSObject doesNotRecognizeSelector:
- Animal NSObject class
- OS_xpc_serializer OS_xpc_object dealloc
- OS_object NSObject dealloc
+ OS_xpc_payload NSObject class
- OS_xpc_payload OS_xpc_payload dealloc
可以看到,当一个方法没有找到后,除了会经过一次动态方法解析后,还会在调用forwardingTargetForSelector
和methodSignatureForSelector
这两个方法,这两个方法便是我们今天讨论的消息转发。
forwardingTargetForSelector--消息快速转发
看到苹果官方文档,他对forwardingTargetForSelector的描述是这样的。
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)
If you implement this method in a non-root class, if your class has nothing to return for the given selector then you should return the result of invoking super’s implementation.
This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.
简单来说,我们可以实现这个方法,返回一个有实现目标selector的对象,当一个方法在当前类没有找到实现,并且动态方法决议并没有处理这一selector,此时便会来到快速转发流程,如果我们在类中实现了forwardingTargetForSelector
并返回一个非空且不是本类的对象,那么这个方法就会交给返回对象处理。
@implementation Animal
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(eat)) {
return [Cat alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
控制台打印
2020-10-08 10:29:40.507728+0800 消息转发[96382:13908868] -[Cat eat]
这样的话,一个原本会导致崩溃的方法便转移给了另一个类处理,这便是快速转发的流程。
虽然文档说不能转移给self本身,但实际上,我们使用runtime的特性在
forwardingTargetForSelector
中使用addmethod方法动态地当前类添加方法,此时便不会进入死循环。
methodSignatureForSelector--慢速消息转发
This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.
如果开发者依然没有处理快速转发,那么消息流程就会来到慢速转发,也就是是methodSignatureForSelector
方法,这个方法要求返回的是一个函数签名,不仅如此,这个方法还需要搭配另一个方法forwardInvocation
使用(如果没有实现依然崩溃),如果我们实现了这两个方法,那么就算我们没有实现当前方法,系统也会把这个事物当作漂流瓶扔出去,而不会导致程序崩溃。我们通过代码体验。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(eat)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"__%s__", __func__);
}
运行结果:
2020-10-09 10:06:37.399345+0800 消息转发[23495:14548357] __-[Animal forwardInvocation:]__
当然,我们也可以对当前事物进行处理。
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"__%s__", __func__);
anInvocation.selector = @selector(eatFood);
[anInvocation invoke];
}
- (void)eatFood {
NSLog(@"%s", __func__);
}
或者交给另外的对象接收:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"__%s__", __func__);
anInvocation.target = [Cat alloc];
[anInvocation invoke];
}
我们可以在这里记录程序方法调用失败的错误信息。
doesNotRecognizeSelector -- 方法没有处理
doesNotRecognizeSelector
的方法定义在objc4
源码中。
// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
这便是我们常见的方法调用失败是控制台打印的错误信息,至此,当来到这里时程序崩溃,退出循环。
总结
当慢速查找没有找到类的实现,方法会依次经过动态方法决议、消息快速转发、消息慢速转发。