iOS 消息转发机制

上节(iOS 消息查找流程)我们讲到,在iOS中对象调用方法,会经历方法的查找,如果查找到方法的IMP,那么就返回并执行这个IMP,如果没有找到,那么就会进入方法转发的流程,这就是我们今天需要探究的内容,iOS消息转发机制。

注 一下代码均来自objc-750源码。

我们再来回顾一下方法查找流程的代码。以下代码只摘录出主要部分

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    。。。省略
    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    。。。省略
}

我们可以发现在查找到方法的实现后,调用了log_and_fill_cache函数,用来做缓存填充和log输出,接下来我们来看一下这个函数的实现。

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);
}

如果objcMsgLogEnabled为真,那么将会输出一写log的日志,我们全局搜索objcMsgLogEnabled发现objcMsgLogEnabled默认为false

bool objcMsgLogEnabled = false;

同时我们发现下面这个函数可以修改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;
}

通过查看logMessageSend源码我们可以发现输出的log在/tmp路径下。

消息转发流程

接下来我们通过代码来看看我们在调用一个没有实现的方法时会有哪些流程。

#import 
#import "JKStudent.h"

extern void instrumentObjcMessageSends(BOOL);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JKStudent *student = [[JKStudent alloc] init];
        instrumentObjcMessageSends(YES);//开启日志打印
        [student saySomething];
        instrumentObjcMessageSends(NO);//关闭日志打印
    }
    return 0;
}

我们将这段代码执行起来,发现他crash了,接下来我们去查看/tmp目录。发现多出来了下面这个文件。


这就是我们代码输出的日志log。

我们截取日志的前面的关键部分如下。

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

从这段日志我们分析出对象在调用未实现的方法时在经历了动态方法解析后还陆续调用了forwardingTargetForSelectormethodSignatureForSelector,最终抛出了我们熟悉的异常。

forwardingTargetForSelector

我们通过查询Objective-C的文档可知

forwardingTargetForSelector:
Returns the object to which unrecognized messages should first be directed.

意思就是在这个方法中我们可以返回一个对象来处理没有实现的消息。这也是消息转发流程的第一步。

如果我们在这一步返回了一个对象去处理这个未处理的消息,那么消息转发的流程到这一步就完成了。接下来这个未处理的消息会在我们返回的对象中,进行消息的查找流程和异常的处理。

methodSignatureForSelector

如果我们在这一步没有返回一个对象,那么会进入第二阶段的消息转发。也就是methodSignatureForSelector。同样我们通过查阅Objective-C文档来看看这个方法的作用。

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对象的情况下(例如,在消息转发期间),也可以使用此方法。 如果您的对象维护一个委托或能够处理它不直接实现的消息,则应重写此方法以返回适当的方法签名。

也就是说这个方法是用来返回一个方法的签名的。

forwardInvocation

我们在文档的底部发现他还有个相关的API叫做forwardInvocation:。他的作用是子类重写该方法并将消息转发到其他的对象。

我们在其文档中发现了一个标识注意的地方。

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对象。 您的重写方法必须为给定的选择器提供适当的方法签名,方法是预先制定一个公式,也可以要求另一个对象提供一个方法签名。

也就是说如果要响应对象本身无法识别的方法,除了重写methodSignatureForSelector以外,还必须重写forwardInvocation
至此我们那个经典的消息转发流程图就解释的通了。

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