本文为L_Ares个人写作,以任何形式转载请表明原文出处。
本节开始说明在动态决议依然没有找到sel
的imp
的话,系统还有没有留给我们机会去防止报错,或者说程序的crash。
其实在看到lookUpImpOrForward
这个慢速查找流程的除了动态决议还有一个方法done
但是你会发现走到这里的都是都是imp
找到了以后的情况。
所以我们可以进去看一看。
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;
,所以if
里面是一定会实现的了。
那么看if
里面,这里会记录一个缓存的状态和日志,那么再进入logMessageSend
看看。
会发现默认的情况下好像是不产生日志的,那么其实可以找一下怎么让它产生日志,所以就搜索一下objcMsgLogEnabled
还有哪里有赋值。
找到如下函数
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;
}
也就是说,如果我给这个函数一个true
它就可以打印日志,日志可以存储到上面图1说的/tmp/msgSends-xxx
的文件里面。
那么到这里,我们新建一个mac
项目,来看一下,当我不给一个方法做实现的时候,log
里面会记录怎样的事情。
于是我新建了一个
并添加一个继承于NSObject
的类JDPerson
,只给一个方法,但是不给实现。
然后把instrumentObjcMessageSends
在main.m
做一个外部声明,这样就可以扩充它,进行自定义的调试。
于是main.m
中就存在如下代码 :
#import
#import
@interface JDPerson : NSObject
- (void)myTest;
@end
@implementation JDPerson
@end
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
JDPerson *person = [[JDPerson alloc] init];
instrumentObjcMessageSends(true);
[person myTest];
instrumentObjcMessageSends(false);
NSLog(@"Hello, World!");
}
return 0;
}
我在方法调用之前打开日志,并在方法调用之后关闭日志,然后去找到日志,看看方法的调用能打印出什么日志。
然后我们前往/tmp
文件夹,找日志,日志的前缀一定是msgSends
。图1代码有说。
找到后打开看一下。
resolveInstanceMethod
这个很熟悉,这是实力方法的动态决议,那么下面的forwardingTargetForSelector
和methodSignatureForSelector
是什么?
这个就是本节要说的,在动态决议没有办法解决imp
找不到的情况以后,系统还会给机会,进行消息转发。
那么关于forwardingTargetForSelector
其实commond + shift + 0
可以打开官方的开发文档看一下。
还有methodSignatureForSelector
其实这两个就是两个消息转发的方式,也是本节的重点,在看图5的log
里面就可以知道,系统在动态决议之后,先进行了forwardingTargetForSelector
,然后再进行了methodSignatureForSelector
,也就是说在forwardingTargetForSelector
没有找到imp
以后才会去找methodSignatureForSelector
。于是我们常称
forwardingTargetForSelector
: 消息的快速转发methodSignatureForSelector
: 消息的慢速转发
来区别它们。
一、forwardingTargetForSelector
那么我们可以在main.m
中按照下面这么去做。
#import
#import
@interface JDStudent : NSObject
//- (void)myTest;
@end
@implementation JDStudent
- (void)myTest
{
NSLog(@"转发到了JDStudent的canDoThis身上");
}
@end
@interface JDPerson : NSObject
- (void)myTest;
@end
@implementation JDPerson
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(myTest)) {
return [JDStudent alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
JDPerson *person = [[JDPerson alloc] init];
[person myTest];
NSLog(@"Hello, World!");
}
return 0;
}
结果图 :
可以看到,JDStudent
也是继承于NSObject
,甚至都没有声明自己有myTest
方法,只是在实现的里面出现过这个方法,经过快速消息转发,一样可以找到sel
对应的imp
。
所以快速消息转发会将消息交给一个你指定的,非nil
、非self
对象来处理。
二、methodSignatureForSelector
当上面的消息快速转发流程还是不能处理方法的imp
找不到的问题的时候,我们就会来到methodSignatureForSelector
。
在官方文档中,你可以看到
返回的是一个信号,那么我们去看一下NSMethodSignature
又是什么,怎么获得?
一个继承于NSObject
的类。
这里就要看一下这个初始化对象的方法了。
+ (NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
也就是说,我们要先获取一个方法imp
的信号,获取的方式是把方法imp
的类型编译告诉他。
那么拿到这个NSMethodSignature
信号之后,我们需要转发的sel
就变成了NSMethodSignature
信号,然后就变成了转发这个信号。
也就是还要调用一步转发信号,看看谁要处理这个信号 :
- (void)forwardInvocation:(NSInvocation *)anInvocation
那么我们修改最开始的代码如下 :
#import
#import
@interface JDStudent : NSObject
//- (void)myTest;
@end
@implementation JDStudent
- (void)myTest
{
NSLog(@"转发到了JDStudent的canDoThis身上");
}
@end
@interface JDPerson : NSObject
- (void)myTest;
@end
@implementation JDPerson
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// if (aSelector == @selector(myTest)) {
// return [JDStudent alloc];
// }
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(myTest)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"%s",__func__);
SEL aSelector = [anInvocation selector];
JDStudent *student = [[JDStudent alloc] init];
if ([student respondsToSelector:aSelector]) {
[anInvocation invokeWithTarget:student];
}else{
[super forwardInvocation:anInvocation];
}
}
@end
//extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
JDPerson *person = [[JDPerson alloc] init];
[person myTest];
NSLog(@"Hello, World!");
}
return 0;
}
执行结果 :
另外,其实只要实现了forwardInvocation
,里面什么都不写,也是没有问题的。
因为慢速消息转发和快速消息转发不一样,慢速消息转发将没有imp
的sel
变成了一个信号,就放在那里,谁可以执行,或者你指定谁去执行都是可以的,反正就是不会走到unrecognized selector sent to instance
这个报错上面去。
三、解决上一节的问题
还记得上一节——动态决议中还有一个问题没有解决吧。
在动态决议中,resolveInstanceMethod
和resolveClassMethod
如果只是实现,但是并没有执行class_addMethod
,也即没有给sel
匹配imp
的情况下,会执行两次。其实这两个问题的原因是一样的,都是sel
匹配不到imp
,所以看一个就可以。
这里我们来解决,我在JDPerson
的实现中添加resolveInstanceMethod
,但是添加class_addMethod
。
#import
#import
@interface JDStudent : NSObject
//- (void)myTest;
@end
@implementation JDStudent
- (void)myTest
{
NSLog(@"转发到了JDStudent的canDoThis身上");
}
@end
@interface JDPerson : NSObject
- (void)myTest;
@end
@implementation JDPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"%s",__func__);
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"%s",__func__);
// if (aSelector == @selector(myTest)) {
// return [JDStudent alloc];
// }
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s",__func__);
if (aSelector == @selector(myTest)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"%s",__func__);
SEL aSelector = [anInvocation selector];
JDStudent *student = [[JDStudent alloc] init];
if ([student respondsToSelector:aSelector]) {
[anInvocation invokeWithTarget:student];
}else{
[super forwardInvocation:anInvocation];
}
}
@end
//extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
JDPerson *person = [[JDPerson alloc] init];
[person myTest];
NSLog(@"Hello, World!");
}
return 0;
}
然后我们看NSLog
的打印 :
这张图13可以说是动态决议及消息转发的一个流程了。
很明显,
(1). 如果我们没有在动态决议中给sel
添加imp
,那么就会进入快速消息转发
(2). 如果快速消息转发还是没有添加sel
的imp
连接,那么就会进入慢速消息转发
(3). 慢速消息转发首先生成了方法信号。
(4). 然后系统有去查询了一次动态方法解析是否执行了class_addMethod
。
(5). 最后,才会转发方法信号。
四、objc_msgSend消息发送流程的总结
-
快速方法查找流程
,objc_msgSend发送了对象
和sel
,也可以添加上参数。然后会根据对象上的isa
找到cls
,然后到cls
的缓存cache
中的buckets
中进行快速方法查找(循环遍历),看有没有bucket
的sel
和我们传入的sel
一致。- 有的话会返回对应的
imp
。 - 如果缓存中没有找到
sel
,那么就会通过CheckMiss
或者JumpMiss
进入方法的慢速查找流程。
- 有的话会返回对应的
-
慢速方法查找流程
,也就是进入方法列表进行查询。慢速查找流程会沿着继承链利用二分法查找算法进行查找sel
的imp
,当cls
的方法列表中没有imp
。- 如果继承链上可以找到
sel
对应的imp
,则返回imp
,退出循环。 - 继承链上的父类、根类的缓存中都没有
imp
,即当发现返回nil
的情况下,退出循环,进入动态决议。
- 如果继承链上可以找到
-
动态决议
,这是系统为我们准备的解决crash
的方法,分为实例动态决议resolveInstanceMethod
和类动态决议resolveClassMethod
,我们可以通过class_addMethod
手动的为sel
添加一个imp
,并且再次进入慢速方法查找流程
。- 如果执行了
class_addMethod
,则sel
可以在找到imp
,并通过慢速方法查找流程
返回imp
。 - 如果没有执行
class_addMethod
,则会进入消息转发流程。
- 如果执行了
-
消息转发
,进入消息转发后,sel
会通过methodSignatureForSelector
变成方法信号,然后会被放在那里,就像一个失物招领,等着人来认领。- 在
methodSignatureForSelector
将sel
变成信号后,系统还会再一次的进行动态决议,查看是否实现了class_addMethod
。- 如果实现了,那么就会把
sel
和imp
连接起来,直接执行imp
。 - 如果没有实现,则会向下进行。
- 如果实现了,那么就会把
- 如果你通过
forwardInvocation
为NSInvocation
找到一个target
,也即是有人认领了方法,那么就查看target
有没有sel
的imp
。- 如果
target
有sel
对应的imp
,则会正常返回imp
。 - 但是,如果
target
也没有这个信号中的sel
的imp
,那么就会进入报错流程unrecognized selector sent to instance
。
- 如果
- 如果
forwardInvocation
中什么都不写,则方法信号就一直漂流四处等待认领,但是不会进入unrecognized selector sent to instance
报错流程。
- 在