In Objective-C, when the essence of calling a method is message passing, the message passing does not find the implementation of the method after three flows of fast lookup -> slow lookup -> dynamic method parsing. Then the next flow, the message forwarding flow, will come.

I. Introduction of message forwarding process

1. InstrumentObjcMessageSends function is introduced

In objc source objc_class. Mm file, there is a instrumentObjcMessageSends function.

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;
}
Copy the code

When flag is YES, all method caches are flushed and synchronized to log files. So where do log files live? Above the instrumentObjcMessageSends function, there is a logMessageSend function.

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;
}
Copy the code

The logMessageSend function is mostly a formatted output of some logs. When the logMessageSend function is called, it saves the log file to the/TMP/path with a file name beginning with msgSends-.

2. Origin of logMessageSend function

So why am I so sure I’m going to use logMessageSend? Remember that in the slow look-up of the -lookupImporForward function, there is a done: process. When an IMP is found, the log_AND_fill_cache function is called to cache the IMP.

The log_and_fill_cache function is implemented as follows:

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);
}
Copy the code

So the first one, when objcMsgLogEnabled && ImplemEnter is true, it calls logMessageSend, and objcMsgLogEnabled, Is it not in internal instrumentObjcMessageSends function assignment? So, instrumentObjcMessageSends function is a similar to open the switch of the log buffer.

3. Test instrumentObjcMessageSends function output log file

Let’s test it. The test code is as follows:

extern void instrumentObjcMessageSends(BOOL flag);

@interface SHPerson : NSObject
- (void)helloWorld;
@end
@implementation SHPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SHPerson *p = [[SHPerson alloc] init];
        instrumentObjcMessageSends(YES);
        [p helloWorld];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}
Copy the code

Note that when testing, don’t use the source code project, otherwise the MSgdoc-file will have no content. See if there is a file starting with msgSends- in the/TMP/path.

Boy, sure enough, let’s take a look at the log in the file.

When we do the dynamic method parsing and still do not find the method implementation, the system will still give the developer a chance, that is, to carry out the message forwarding process. As shown in figure shown in, message forwarding process, there are two main methods of forwardingTargetForSelector respectively: and methodSignatureForSelector:.

2. Message forwarding process

How does the message forwarding process work? Now let’s look at forwardingTargetForSelector: methods and methodSignatureForSelector: method how to use.

1. Quick forwarding process

ForwardingTargetForSelector: method return values for the id, parameters for aSelector. So according to the official notes, my personal understanding is that when this method is implemented, it can forward aSelector and receive objects of type ID, that is, any object. When we return the received object, the received object will continue to look up aSelector, which is a repeat of the previous message passing process.

For example, we now have two objects, SHPerson and SHAnimal respectively, we declare in the SHPerson run method, but not implemented, and implement forwardingTargetForSelector: method. Implement a run method in SHAnimal. The specific code is as follows:

@interface SHAnimal : NSObject
@end
@implementation SHAnimal
- (void)run {
    NSLog(@"%s", __func__);
}
@end
Copy the code
@interface SHPerson : NSObject
- (void)run;
@end
@implementation SHPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector = = @selector(run)) {
        NSLog(@"%s",__func__);
        return [SHAnimal alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end
Copy the code
SHPerson *p = [[SHPerson alloc] init];
[p run];
Copy the code
Print result:2021-12-27 16:28:14.862557+0800 06-Message passing: Message forwarding-01[72288:2241695] -[SHPerson forwardingTargetForSelector:]
2021-12-27 16:28:14.862844+0800 06-Message passing: Message forwarding-01[72288:2241695] -[SHAnimal run]
Copy the code

When we realized in SHPerson didn’t run method, in addition to can do it in that process of dynamic method resolution processing, can also in forwardingTargetForSelector: method for processing. As printed, SHPerson does not implement run, so we manually tell it to look in the SHAnimal object.

The SHAnimal object is the recipient of the current message forward, and many people also refer to it as the standby receiver, or spare.

2. Slow forwarding process

When we are forwardingTargetForSelector: methods for processing, always feel strange. If SHAnimal don’t realize the run method, program crashes, after all is just the spare tire 😂, so we don’t want to in forwardingTargetForSelector: Do processing, then began to enter into the next process, called slow forward process, namely realize methodSignatureForSelector: method, in methodSignatureForSelector: do forwarding processing method.

1. methodSignatureForSelector:

MethodSignatureForSelector: method needs to return a NSMethodSignature object, namely the method signature. It is important to note that methodSignatureForSelector: and forwardingTargetForSelector: can’t exist at the same time, or just walk to forwardingTargetForSelector: , don’t go to methodSignatureForSelector:.

The code is as follows:

@interface SHPerson : NSObject
- (void)run;
@end
@implementation SHPerson
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector = = @selector(run)) {
        NSLog(@"%s",__func__);
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
}
Copy the code

After we run the code, although call methodSignatureForSelector: method, but the program or collapse. Don’t methodSignatureForSelector: methods can’t solve it, I’m watching methodSignatureForSelector: method of documentation, noticed forwardInvocation: method.

2. forwardInvocation:

In implementing methodSignatureForSelector: method at the same time, also must create NSInvocation object. I understand the general meaning is that methodSignatureForSelector: and forwardInvocation: must achieve together, because to achieve the forwardInvocation: Method, which creates the NSInvocation object and passes the NSInvocation object as a parameter to the forwardInvocation: method.

So, we implement the forwardInvocation: method and re-run it.

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);
}
Copy the code

Implements forwardInvocation: after method, indeed not collapsed, and also print methodSignatureForSelector: and forwardInvocation:.

Then why does the forwardInvocation not crash without doing anything? Here is my translation of the official document for forwardInvocation:

  • When an object receives a message with no corresponding method, the runtime system gives the receiver an opportunity to delegate the message to another receiver. It delegates the message by creating a NSInvocation object representing the message and sending a forwardInvocation: message to the receiver, which includes the NSInvocation object as a parameter. The receiver’s forwardInvocation: method then has the option to forward the message to another object. (If the object is also unable to respond to the message, it will also have the opportunity to forward it.)

  • ForwardInvocation: Messages thus allow one object to have relationships with other objects that will act on its behalf for some messages. In a sense, forwarding objects can “inherit” certain characteristics of the object to which messages are forwarded.

  • To respond to your object itself can’t identify method, in addition to forwardInvocation: besides, you must also cover methodSignatureForSelector:.

  • Forwarding message mechanism using from methodSignatureForSelector: access to the information to create the NSInvocation object is forwarding. Your override method must provide the appropriate method signature for a given selector, either by pre-specifying a method or by asking another object for a method.

forwardInvocation:The implementation of the method has two tasks:

  1. Positioning can respondanInvocationObject of the encoded message in. This object need not be the same for all messages.
  2. Use the call to send a message to the object.anInvocationThe result is saved, and the runtime system extracts it and passes it to the original sender.

So, what does that mean? Let’s look at a simple implementation of forwardInvocation:.

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);

    SEL aSelector = [anInvocation selector];
    SHAnimal *forward = [SHAnimal alloc];

    if ([forward respondsToSelector:aSelector]) {
        [anInvocation invokeWithTarget:forward];
    }else{[superforwardInvocation:anInvocation]; }}Copy the code

With this code and print, it is exactly the comments that confirm the official document. ForwardInvocation: once implemented, the final message forwarding can be handled in the method via the NSInvocation object.

NSInvocation is the equivalent of business, you just need to tell it if the message is to be forwarded if needed, as in the example above. Don’t need to be forwarded, NSInvocation object will be very good, regardless of anything, but not to crash the program, as long as the realized methodSignatureForSelector:, return method signatures, and create NSInvocation object, will not collapse.

ForwardInvocation: creates an NSInvocation object for us and passes it to us for the final message forwarding from the NSInvocation object.