preface

If you want to be an iOS developer, you have to read source code. The following is a small series that the author combed in OC source code exploration — Class and Object, welcome you to read and correct, but also hope to help you.

  1. Object creation for OC source analysis
  2. OC source code analysis of ISA
  3. OC source code analysis and such structural interpretation
  4. Caching principle of OC source analysis method
  5. OC source code analysis method search principle
  6. Analysis and forwarding principle of OC source code analysis method

The OC method is called by objc_msgSend (or objc_msgSendSuper, or objc_msgSend_stret, or objc_msgSendSuper_stret), which sends a message named SEL to the caller. The function is then executed. If you can’t find IMP, the method will be analyzed, which is equivalent to providing a fault tolerant processing; Method analysis, if still can not find IMP, there is a last chance, that is the message forward.

Method of the search process in the OC source analysis method of the search principle in this article, this article will be in-depth analysis of the method of analysis and forwarding.

Let’s get down to business.

Note that the source code I used was ObjC4-756.2.

1. Analysis of methods

Method resolver (also known as message resolution, also known as method resolution), which is based on the failure of the method search results, entry source code is as follows:

    // IMP is not found in the cache + method list of the root class
    if(resolver && ! triedResolver) { runtimeLock.unlock(); resolveMethod(cls, sel, inst); runtimeLock.lock();// Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }
Copy the code

It basically calls the resolveMethod function. After the resolveMethod function is processed, a retry is performed (retracing the method lookup process). The triedResolver variable causes the message to be parsed only once.

1.1 resolveMethod

ResolveMethod function resolveMethod

static void resolveMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(cls, sel, inst);
        if(! lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { resolveInstanceMethod(cls, sel, inst); }}}Copy the code

CLS is a metaclass, and CLS is a metaclass.

  • It is not a metaclass, which means the instance method is called, so executeresolveInstanceMethodfunction
  • Is a metaclass, which means a class method is called and executedresolveClassMethodFunction, and then if I still don’t find itIMP, and then executeresolveInstanceMethodFunctions;

Let’s start with instance methods

1.2 Instance method analysis

ResolveInstanceMethod resolveInstanceMethod

static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // if you don't implement class methods +(BOOL)resolveInstanceMethod:(SEL) SEL
        // NSObject is also implemented, so you don't normally go there
        // Notice that the first argument passed in here is CLS ->ISA(), which is the metaclass
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // Call class method: +(BOOL)resolveInstanceMethod:(SEL) SEL
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Find imp again (sel this time, not resolveInstanceMethod)
    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() ? '+' : The '-', 
                         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() ? '+' : The '-', cls->nameForLogging(), sel_getName(sel)); }}}Copy the code

The resolveInstanceMethod function calls lookUpImpOrNil twice:

  • The first call is to determine whether the class (including its parent class, up to the root class) is implemented+(BOOL)resolveInstanceMethod:(SEL)selClass method
    • SEL_resolveInstanceMethodThe equivalent of@selector(resolveInstanceMethod:).NSObjectClass has methods that implement this classNO, will affect whether or not to print), so it will generally continue down.
  • The purpose of the second call is to check if there areselThe correspondingIMP. If you are in+(BOOL)resolveInstanceMethod:(SEL)selAdd theselFunction address ofIMPAnd now I’m going to look for this againIMPYou can find it.

Note that in both cases, resolver is NO and therefore does not trigger the resolution of a message when it calls lookUpImpOrForward. , root class “cache and method list to find IMP, not found will trigger message forwarding.

lookUpImpOrNil

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}
Copy the code

This determines whether the IMP was forwarded by a message, and if so, does not return.

1.3 Class method analysis

ResolveClassMethod (resolveClassMethod);

// CLS is a metaclass because class methods are stored in the metaclass
static void resolveClassMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // if you don't implement class methods +(BOOL)resolveClassMethod:(SEL) SEL
        // NSObject is also implemented, so you don't normally go there
        // Notice that the first argument here is CLS, which is a metaclass
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        // Get the object of the metaclass, the class. In other words, nonmeta is inST
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if(! nonmeta->isRealized()) { _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // Call class method: +(BOOL)resolveClassMethod:(SEL) SEL
    bool resolved = msg(nonmeta, SEL_resolveClassMethod, sel);

    // find imp again
    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() ? '+' : The '-', 
                         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() ? '+' : The '-', cls->nameForLogging(), sel_getName(sel)); }}}Copy the code

ResolveInstanceMethod +(BOOL)resolveClassMethod:(SEL) SEL

Let’s review the resolution of class methods by the resolveMethod function

// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(cls, sel, inst);
if(! lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
{
    // CLS is a metaclass, that is, NSObject calls resolveInstanceMethod:
    resolveInstanceMethod(cls, sel, inst);
}
Copy the code

If the IMP of the class method is still not found, the resolveInstanceMethod function will be executed again. Unlike instance methods, the CLS is metaclass, so MSG (CLS, SEL_resolveInstanceMethod, sel); Sending a resolveInstanceMethod: message inside the metaclass means that the root class calls the resolveInstanceMethod: method (this time only in the root class’s classification), and caching the IMPs that lookup class methods occurs only in the root metaclass and root class. And method list to find class method IMP is respectively in the “metaclass, metaclass parent class,… , root metaclass, root class.

In short, when we call a class method that is not implemented in the class and not handled in the resolveClassMethod, we end up calling an instance method of the same name on the root class (NSObject).

1.4 For example

Through the above analysis, I believe that we have a certain understanding of the analysis of the method, let’s digest the whole simple example.

@interface Person : NSObject

+ (void)personClassMethod1;
- (void)personInstanceMethod1;

@end

@implementation Person

@end
Copy the code

A simple Person class with a class method and an instance method, but neither is implemented.

Then add the parsing of the two methods:

- (void)unimplementedMethod:(SEL)sel {
    NSLog(@"Didn't happen? It's okay. Never fall apart.");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"Dynamic instance method parsing: %@", NSStringFromSelector(sel));
    if (sel == @selector(personInstanceMethod1)) {
        IMP methodIMP = class_getMethodImplementation(self, @selector(unimplementedMethod:));
        Method method = class_getInstanceMethod(Person.class, @selector(unimplementedMethod:));
        const char *methodType = method_getTypeEncoding(method);
        return class_addMethod(Person.class, sel, methodIMP, methodType);
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"Dynamic class method resolution: %@", NSStringFromSelector(sel));
    if (sel == @selector(personClassMethod1)) {
        IMP methodIMP = class_getMethodImplementation(self, @selector(unimplementedMethod:));
        Method method = class_getInstanceMethod(Person.class, @selector(unimplementedMethod:));
        const char *methodType = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("Person"), sel, methodIMP, methodType);
    }
    return [super resolveClassMethod:sel];
}
Copy the code

Look at the print:

From the source code analysis of class method parsing, we know that we can also put the processing of the Person class method in the NSObject class resolveClassMethod: or resolveInstanceMethod:, Do the same (remember to remove the resolveClassMethod: process from the Person class). I’ll skip it here.

2 Message Forwarding

Method calls through the search, parsing, if still not found IMP, will come to the message forwarding process. Its entry is later in lookUpImpOrForward

// No implementation found, and method resolver didn't help.     
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
Copy the code

2.1 Start and end of message forwarding

_objc_msgForward_impcache is an assembly function, using arm64 architecture as an example, its source code is as follows:

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache
Copy the code

__objc_msgForward_impcache internally calls __objc_msgForward

ENTRY __objc_msgForward

adrp	x17, __objc_forward_handler@PAGE
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
	
END_ENTRY __objc_msgForward
Copy the code

The main thing this function does is get the address of the _objc_forward_handler function by offsetting the page address.

Description:

  • adrpA wide range of address reading instructions in pages, herepispageThe meaning of
  • ldrSimilar tomovandmvn, when the immediate number (__objc_msgForwardIs in the[x17, __objc_forward_handler@PAGEOFF].PAGEOFFIs the page address offset value) greater thanmovandmvnUse the maximum number that can be manipulatedldr.

In OBJC2, _objc_forward_handler is actually the objc_defaultForwardHandler function, with the following source code:

// Default forward handler halts the process.
__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : The '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
Copy the code

Does that sound familiar? When we call an unimplemented method, the error is “unrecognized selector sent to…”

But here comes the question, what about the agreed message forwarding process? How can this end when it just started? No hurry, no hurry, just watch.

2.2 Call stack of message forwarding

Let it crash! Let it crash!

The only thing that was found related to message forwarding before the crash was that the _CF_forwarding_prep_0 and ___forwarding___ functions were called. Unfortunately, these two functions are not open source.

Since the crash information does not help, print the specific call information.

The log_and_fill_cache function is used to print the log_and_fill_cache function.

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;

// Define SUPPORT_MESSAGE_LOGGING to enable NSObjCMessageLoggingEnabled
#if! TARGET_OS_OSX
#   define SUPPORT_MESSAGE_LOGGING 0
#else
#   define SUPPORT_MESSAGE_LOGGING 1
#endif
Copy the code

The key function for printing is logMessageSend, but it is controlled by SUPPORT_MESSAGE_LOGGING and objcMsgLogEnabled.

Follow up with SUPPORT_MESSAGE_LOGGING

#if! DYNAMIC_TARGETS_ENABLED
    #define TARGET_OS_OSX               1.#endif
    
#ifndef DYNAMIC_TARGETS_ENABLED
 #define DYNAMIC_TARGETS_ENABLED   0
#endif
Copy the code

The value of TARGET_OS_OSX is 1, so SUPPORT_MESSAGE_LOGGING is also 1!

If you can set objcMsgLogEnabled to true, you can obviously print the call information. Through the global search objcMsgLogEnabled, we found the instrumentObjcMessageSends this critical 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

Now that’s easy! Go to main.m and add the following code

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        instrumentObjcMessageSends(true);
        [Person personClassMethod1];
        instrumentObjcMessageSends(false);
    }
    return 0;
}
Copy the code

Run the project until it crashes again. At this point the function call stack is printed and the log file location is indicated in the logMessageSend function

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

Open Finder, CMD + Shift + G shortcut, type/TMP /msgSends, find the latest log file (maximum number)

The print result is as follows:

Can be seen from the log, and forward the relevant method is forwardingTargetForSelector and methodSignatureForSelector, corresponding to the message of the forward fast forwarding process and slow process, and then to analyze the two methods.

2.3 Fast message forwarding

ForwardingTargetForSelector: corresponding is fast forwarding message flow, it simply returns nil in source (may be classified in subclass or rewrite)

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

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

However, we can find instructions in the development documentation (CMD + Shift + 0 shortcut).

In a nutshell, forwardingTargetForSelector: mainly is to return a new receiver, to deal with sel the current class cannot process the messages, if can’t handle, will get the inefficient forwardInvocation:. In terms of efficiency, forwardingTargetForSelector: lead forwardInvocation: an order of magnitude, therefore, it is best not to use the latter approach to the forwarding of a message logic.

About forwardingTargetForSelector: return of the new receiver, it is necessary to pay attention to the points:

  • Never returnselfOtherwise it will fall into an infinite loop;
  • If not, returnnilOr,[super forwardingTargetForSelector:sel](in the case of non-root classes), will walkmethodSignatureForSelector:Slow forwarding process;
  • If you have thisreceiverIn this case, the command is executedobjc_msgSend(newReceiver, sel, ...)It must have the same method signature (method name, parameter list, return value type) as the method being called.

2.3.1 For example

We can try it out. Here’s how it works

@interface ForwardObject : NSObject

@end

@implementation ForwardObject

+ (void)personClassMethod1 {
    NSLog(@"Class method forward to %@, execute %s", [self className], __FUNCTION__);
}

- (void)personInstanceMethod1 {
    NSLog(@"Instance method forward to %@, execute %s", [self className], __FUNCTION__);
}

@end

@interface Person : NSObject

+ (void)personClassMethod1;
- (void)personInstanceMethod1;

@end

@implementation Person

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"Instance method starts forwarding");
    return [ForwardObject alloc];
}

+ (id)forwardingTargetForSelector:(SEL)sel {
    NSLog(@"Class method starts forwarding");
    return [ForwardObject class];
}

@end
Copy the code

Obviously, the ForwardObject class that handles the forward message has the same class method and instance method of the Person class. Now start the validation and the result is as follows:

And it works! Next, look at the slow forwarding process of messages.

2.4 Slow forwarding of messages

If forwardingTargetForSelector: no processing messages (e.g., returns nil), will start slow forward process, namely methodSignatureForSelector: method, also need to rewrite in subclasses or classification

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}
Copy the code

Through reading the official documents, we draw the following conclusions:

  • methodSignatureForSelector:Method is toforwardInvocation:Method collocation use, the former need us according toselReturns a method signature, which encapsulates the method signature into oneNSInvocationObject and take it as a parameter.
  • If there’s a target object that can handle itInvocationIn thesel.InvocationYou can assign this object to handle; Otherwise, it is not processed.
    • InvocationMultiple object processing can be assigned

Note: The slow forwarding process of messages has low performance, so if you can, you should dispose of messages as early as possible (for example, during method parsing, or during a fast forwarding process of messages).

2.4.1 For example

This can also be verified for slow processes. Here’s a change to the Person class in the quick forward example:

@implementation Person

// MARK: Slow forwarding -- class method

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"Class method slow forwarding: %s, sel: %@", __FUNCTION__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(personClassMethod1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL aSelector = [anInvocation selector];
    NSLog(@"Class method slow forwarding: %s, sel: %@", __FUNCTION__, NSStringFromSelector(aSelector));
    id target = [ForwardObject class];
    if ([target respondsToSelector:aSelector]) [anInvocation invokeWithTarget:target];
    else [super forwardInvocation:anInvocation];
}

// MARK: Slow forwarding -- instance method

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"Instance method slow forwarding: %s, sel: %@", __FUNCTION__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(personInstanceMethod1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL aSelector = [anInvocation selector];
    NSLog(@"Instance method slow forwarding: %s, sel: %@", __FUNCTION__, NSStringFromSelector(aSelector));
    ForwardObject *obj = [ForwardObject alloc];
    if ([obj respondsToSelector:aSelector]) [anInvocation invokeWithTarget:obj];
    else [super forwardInvocation:anInvocation];
}

@end
Copy the code

The result, as you can see below, apparently didn’t crash either.

For those unfamiliar with method signature type encodings, see apple’s official type encodings guide

3 summary

To sum up, when we call a method, we will first search the method. If the search fails, we will parse the method. At this time, OC will give us a chance to process SEL. You can add an IMP to the resolveInstanceMethod :(class method corresponding to resolveClassMethod :); If you don’t seize this opportunity, that is, parse fails, will come forward stage, this stage, there are two opportunities to deal with sel, is fast forwarding forwardingTargetForSelector: respectively, and the methodSignatureForSelector: slowly forward. Of course, if you pass up these opportunities, OC will have to crash the program.

The following diagram summarizes the parsing and forwarding flow of the method

4 Discussion

4.1 Why Is the Message forwarding Mechanism Introduced?

There is no way to determine the implementation address of a method until it is called. At runtime, when the method is called, we don’t really know whether it has an implementation and its implementation address. This is known as “dynamic binding”.

At compile time, if the compiler finds that the method does not exist, it will directly report an error; Also, at runtime, there is doesNotRecognizeSelector handling.

Before throwing doesNotRecognizeSelector, OC uses its dynamic binding feature to introduce message forwarding mechanism, which gives us extra opportunities to process messages (parsing or forwarding). This approach is obviously more comprehensive and reasonable.

5 PS

  • The source code project has been placedgithubThe stamp, pleaseObjc4-756.2 – the source code
  • You can also download apple’s official ObjC4 source code to study.
  • Reprint please indicate the source!