iOS - 消息转发机制

在iOS - 方法查找流程一文中,提到过当查找不到方法时会进行动态方法决议,如果动态方法决议也找不到该怎么办呢?那么我们就具体分析一下动态方法决议找不到之后,系统会做些什么.

1、动态方法决议
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

通过判断时实例方法还是类方法我们分别调用_class_resolveInstanceMethod和_class_resolveClassMethod;

1.1、实例方法: _class_resolveInstanceMethod
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
  • 判断是否实现 SEL_resolveInstanceMethod 方法, 在NSObject 已经实现这个方法 , 默认返回为 NO,因此继承NSObject的类不会走该方法;
  • 向类发送SEL_resolveInstanceMethod消息,调用该方法;
  • 调用解析器方法 ( SEL_resolveInstanceMethod ) 完成后 , 重新检查有没有这个 sel 的 imp;
  • imp如果找到,则输出动态解析对象方法成功的日志
  • 如果imp 没有找到,则输出虽然实现了+(BOOL)resolveInstanceMethod:(SEL)sel,并且返回了 YES,但并没有查找到imp的日志
    因此为了避免crash我们可以重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法,使其找到相应的方法:
实例:

在NSObject类中添加两个方法

@interface NSObject (E)

- (void)speak;

+ (void)sing;

@end
- (void)speak{
    NSLog(@"%s",__func__);
}

+ (void)sing{
    NSLog(@"%s",__func__);
}

创建Person类继承于NSObject

@interface Person : NSObject

- (void)eat;

+ (void)shopping;

@end
@implementation Person
//Person中没有方法实现
@end

创建Student类继承于Person

@interface Student : Person

- (void)study;

+ (void)play;

@end

@implementation Student

- (void)study {
    NSLog(@"%s",__func__);
}

+ (void)play {
    NSLog(@"%s",__func__);
}

@end

//调用eat方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"
        Student *student = [[Student alloc] init];
        [student eat];
#pragma clang diagnostic pop
    }
    return 0;
}

由于Person类中并没有eat方法的实现,因此此处势必找不到imp,程序会崩溃

2020-01-29 20:54:35.919681+0800 Test[3061:196810] -[Student eat]: unrecognized selector sent to instance 0x10060ed70
2020-01-29 20:54:35.921386+0800 Test[3061:196810] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Student eat]: unrecognized selector sent to instance 0x10060ed70'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff39d398ab __exceptionPreprocess + 250
    1   libobjc.A.dylib                     0x00007fff6fff3805 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff39db8b61 -[NSObject(NSObject) __retain_OA] + 0
    3   CoreFoundation                      0x00007fff39c9dadf ___forwarding___ + 1427
    4   CoreFoundation                      0x00007fff39c9d4b8 _CF_forwarding_prep_0 + 120
    5   Test                                0x0000000100000c56 main + 86
    6   libdyld.dylib                       0x00007fff713617fd start + 1
    7   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

那么如何能使程序不崩溃呢?由上面的分析可知,如果我们在Student类中重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法并在其中实现eat方法不就可以找到imp了吗

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"来了:%s - %@",__func__,NSStringFromSelector(sel));
    if (sel == @selector(eat)) {
//当调用eat方法时,调用study的方法实现
        NSLog(@"你妈喊你回家吃饭了");
        IMP studyIMP = class_getMethodImplementation(self, @selector(study));
        Method studyMethod = class_getInstanceMethod(self, @selector(study));
        const char *studyType = method_getTypeEncoding(studyMethod);
        return class_addMethod(self, sel, studyIMP, studyType);
    }

    return [super resolveInstanceMethod:sel];
}

打印结果:

2020-01-29 21:00:35.862697+0800 Test[3102:199221] 来了:+[Student resolveInstanceMethod:] - eat
2020-01-29 21:00:35.863553+0800 Test[3102:199221] 你妈喊你回家吃饭了
2020-01-29 21:00:35.863698+0800 Test[3102:199221] -[Student study]

由结果可知:重写resolveInstanceMethod的确可以避免程序崩溃

1.2、类方法: _class_resolveClassMethod
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
  • 跟实例方法下的动态决议一样,区别在于此时调用的是SEL_resolveClassMethod方法;
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}
实例:

调用shopping方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"
        Student *student = [[Student alloc] init];
//        [student eat];
        [Student shopping];
#pragma clang diagnostic pop
    }
    return 0;
}
+ (BOOL)resolveClassMethod:(SEL)sel{

    NSLog(@"来了类方法:%s - %@",__func__,NSStringFromSelector(sel));

     if (sel == @selector(shopping)) {
         NSLog(@"开心购物");
//类方法存在元类中 因此要调用元类中的方法objc_getMetaClass("Student")
         IMP playIMP = class_getMethodImplementation(objc_getMetaClass("Student"), @selector(play));
         Method playMethod = class_getClassMethod(objc_getMetaClass("Student"), @selector(play));
         const char *playType = method_getTypeEncoding(playMethod);
         return class_addMethod(objc_getMetaClass("Student"), sel, playIMP, playType);
     }
     return [super resolveClassMethod:sel];
}

打印结果:

2020-01-29 21:15:25.626454+0800 Test[3139:205404] 来了类方法:+[Student resolveClassMethod:] - shopping
2020-01-29 21:15:25.627208+0800 Test[3139:205404] 开心购物
2020-01-29 21:15:25.627312+0800 Test[3139:205404] +[Student play]

如果在动态决议下依旧找不到该方法时,系统又会怎么处理呢?接下来将会进入到详细转发机制.

2、消息转发
2.1准备阶段

在iOS - 方法查找流程一文中,我们提到了,在方法的查找过程中会调用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 (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}
bool objcMsgLogEnabled = false;
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;
}
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;
}

由源码可知,是否打印日志取决于objcMsgLogEnabled的值,而objcMsgLogEnabled的值则取决于instrumentObjcMessageSends方法,因此我们可以通过instrumentObjcMessageSends方法来设置是否输出日志,且该日志存储在/tmp目录下文件名为msgSends-"一串数字";

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"
        Student *student = [[Student alloc] init];
        instrumentObjcMessageSends(true);
        [student eat];
        instrumentObjcMessageSends(false);
#pragma clang diagnostic pop
    }
    return 0;
}

查看日志

+ Student NSObject resolveInstanceMethod:
+ Student NSObject resolveInstanceMethod:
- Student NSObject forwardingTargetForSelector:
- Student NSObject forwardingTargetForSelector:
- Student NSObject methodSignatureForSelector:
- Student NSObject methodSignatureForSelector:
- Student NSObject class
+ Student NSObject resolveInstanceMethod:
+ Student NSObject resolveInstanceMethod:
- Student NSObject doesNotRecognizeSelector:
- Student NSObject doesNotRecognizeSelector:
- Student NSObject class

由日志我们可以看出,系统不但执行了动态决议的方法还执了一系列的其他的方法;

2.2快速转发
forwardingTargetForSelector
+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

发现在这里什么都看不出来,那么我们就去官方文档查看该方法的具体解释是什么.

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.
大致的意思是:这个方法给了未知消息在进入forwardInvocation:方法前一次重新定义的机会,当您只想将消息重定向到另一个对象时,这非常有用,并且可以比常规转发快一个数量级。当转发的目标是NSInvocation对象时,或者在转发带有参数或返回值时,它不起作用

例如我们创建一个Teacher类继承于NSObject

@interface Teacher : NSObject

@end

@implementation Teacher

- (void)eat{
    NSLog(@"%s",__func__);
}
@end

在Student类中我们不在实现动态决议的方法,而是实现forwardingTargetForSelector;

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(eat)) {
        return [Teacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

打印结果:

2020-01-29 22:16:17.270894+0800 Test[3315:238995] -[Student forwardingTargetForSelector:] -- eat
2020-01-29 22:16:17.271522+0800 Test[3315:238995] -[Teacher eat]

由此可知,我们可以利用这个方法将A类中的方法转发到B类中去实现;

2.3慢速转发
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.
大致意思是: 此方法用于协议的实现。 在必须创建NSInvocation对象的情况下(例如,在消息转发期间),也可以使用此方法。 如果您的对象维护一个委托或能够处理它不直接实现的消息,则应重写此方法以返回适当的方法签名。

resolveInstanceMethod

To respond to methods that your object does not itself recognize, you must override methodSignatureForSelector: in addition to forwardInvocation:. The mechanism for forwarding messages uses information obtained from methodSignatureForSelector: to create the NSInvocation object to be forwarded. Your overriding method must provide an appropriate method signature for the given selector, either by pre formulating one or by asking another object for one.

大致意思是: 要响应对象本身无法识别的方法,除了forwardInvocation:外,还必须重写methodSignatureForSelector:。 转发消息的机制使用从methodSignatureForSelector:获得的信息来创建要转发的NSInvocation对象。 您的重写方法必须为给定的选择器提供适当的方法签名,方法是预先制定一个公式,也可以要求另一个对象提供一个方法签名。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(eat)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
     NSLog(@"%s",__func__);
     SEL aSelector = [anInvocation selector];

    if ([[Teacher alloc] respondsToSelector:aSelector])
        [anInvocation invokeWithTarget:[Teacher alloc]];
    else
        [super forwardInvocation:anInvocation];
}

关于v@:的含义,请查看官方文档:方法签名encode表
打印结果:

2020-01-29 22:29:59.982181+0800 Test[3360:247313] -[Student methodSignatureForSelector:] -- eat
2020-01-29 22:29:59.983108+0800 Test[3360:247313] -[Student forwardInvocation:]
2020-01-29 22:29:59.983341+0800 Test[3360:247313] -[Teacher eat]

即使forwardInvocation中不是实现后续方法也不会崩溃,这次的转发作用和第二次的比较类似,都是将 A 类的某个方法,转发到 B 类的实现中去。不同的是,第三次的转发相对于第二次更加灵活,forwardingTargetForSelector: 只能固定的转发到一个对象;forwardInvocation: 可以让我们转发到多个对象中去.

2.4消息无法处理
doesNotRecognizeSelector
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

报出异常错误;

3.总结
  • 在动态方法决议无法处理时,将会执行消息转发机制,首先执行的是快速转发,即调用forwardingTargetForSelector;
  • 当快速转发也无法对消息进行处理时,则执行慢速转发,调用methodSignatureForSelector和resolveInstanceMethod;
  • 当慢速转发也无法对消息进行处理时,则抛出异常,调用doesNotRecognizeSelector方法,打印错误信息;
附:消息转发流程图
消息转发流程.png

你可能感兴趣的:(iOS - 消息转发机制)