Objc_msgSend is a fast lookup and a slow lookup. In the case that neither of these two methods can be found, Apple gives two suggestions: dynamic method resolution and message forwarding.

  • Dynamic method resolution: If the slow search process is not found, a dynamic method is executed to decide to query again after a chance is given
  • forward: If the dynamic method resolution still does not find an implementation, the message is forwarded

If neither of these suggestions does anything, it will report an unrecognized selector crash that is common in our daily development.

The introduction of

The test code is as follows:

//LGPerson.h
@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayNB;
@end

Lgperson.m is not implemented

//main.m
LGPerson *person = [LGPerson alloc];
[LGPerson sayNB];
[person sayHello];
Copy the code

callClass methods sayNBandExample method sayHelloError as follows:

Method failed to implement

The __objc_msgForward_impcache method fails to implement an unrecognized selector.

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b   __objc_msgForward

END_ENTRY __objc_msgForward_impcache

/ / 👇
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 following c/ C ++ implementation is implemented by calling the objc_defaultForwardHandler method:

// Default forward handler halts the process.
__attribute__((noreturn, cold)) 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

This is one of the most common mistakes we make in daily development: failure to implement functions, running programs, and crashes. Here’s how many times you can avoid a crash before you crash

Dynamic method resolution and message forwarding

We got three more chances before we crash.

  • The first chance isDynamic method resolution
    • Class method +(BOOL)resolveClassMethod:(SEL) SEL;
    • Example method + (BOOL)resolveInstanceMethod:(SEL) SEL;
  • forward
    • A second chanceFast forward
      • – (id) forwardingTargetForSelector (SEL) aSelector;
    • Third chanceSlowly forward
      • – (NSMethodSignature *) methodSignatureForSelector (SEL) aSelector;
      • – (void) forwardInvocation (anInvocation NSInvocation *);

The following are explained separately.

First chance dynamic method resolution

Objc-runtimenew.mm is an objc-runtimenew.mm object with a dynamic method resolution. If objc-runtimenew.mm is an objc-runtimenew.mm object, it is an objc-runtimenew.mm object.

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    // Object methods -- save in class
    if (! cls->isMetaClass()) { // Classes are not metaclasses and call object parsing methods
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {/ / is a metaclass
    // Class methods -- save in metaclasses
    Class methods exist in metaclasses as instance methods
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        // Why this line of code? Class methods are object methods in the metaclass, so you still need to query the dynamic method resolution of object methods in the metaclass
        if(! lookUpImpOrNil(inst, sel, cls)) {// If not found or empty, look in the object method resolution method of the metaclass
        //⚠️⚠️⚠️ the recovery metaclass inheritance chain goes up to NSObject and goes to the instance method of NSObjectresolveInstanceMethod(inst, sel, cls); }}// chances are that calling the resolver have populated the cache
    // so attempt using it
    // If the method resolution points its implementation to another method, the method lookup process continues
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
Copy the code

Description:

  • Determine whether a class is a metaclass

    • If it isclass, the implementation ofInstance methodsDynamic method resolutionresolveInstanceMethod
    • If it isThe metaclass, the implementation ofClass methodDynamic method resolutionresolveClassMethod
      • If you’re in a metaclassCould not findOr foremptyWhile the,The metaclasstheInstance methodsDynamic method resolutionresolveInstanceMethod, mainly because the metaclass’s recovery metaclass inheritance chain goes up toNSObject classWould go,NSObject classtheInstance methods.

  • If the dynamic method resolution points its implementation to another method, the lookup continues for the specified IMP, that is, the lookUpImpOrForward process continues slowly

Instance methods

Let’s look at the resolveInstanceMethod method in objc runtime – new. Mm implementation in the source code:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);//oc's resolveInstanceMethod: method
    
    // look is resolveInstanceMethod -- just like sending a message before it is fault-tolerant
    if(! lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {// Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel); // Send the resolve_sel message (call oc's resolveInstanceMethod: method)

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    
    // Add imp to the resolveInstanceMethod method. If there is a save lookUpImpOrNil again the imp corresponding to sel will be found
    // Find say666 again
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    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

Description:

  • Before sending the resolveInstanceMethod message, we need to check whether the CLS class has the implementation of the resolveInstanceMethod method. That is, lookUpImpOrNil is used and lookUpImpOrForward is used to find the resolveInstanceMethod method

    • If not, return directly
    • If yes, send itresolveInstanceMethodThe message
  • The lookUpImpOrNil method is used to look up the instance method in the lookUpImpOrForward slow lookup process

Test code crash rescue

For instance say666, we can override the resolveInstanceMethod method in LGPerson and point it to another method implementation. Point the implementation of the instance method say666 to the implementation of the sayMaster method as shown below

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"% @", NSStringFromSelector(sel));
        // Get imp of sayMaster method
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        // Get the sayMaster instance method
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        // Get sayMaster's rich signature
        const char *type = method_getTypeEncoding(sayMethod);
        // Point sel implementation to sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}
Copy the code

Run it again, and the result is printed as followsAs you can see from the results,resolveInstanceMethodThe “coming” is printed twice in the dynamic resolution method. Why? You can see this by looking at the stack information

  • [first dynamic resolution] The first “coming” is the dynamic method resolution when searching for say666

  • Second second dynamic resolution 】 【 “come” is slowly forward process invokes the CoreFoundation framework of NSObject (NSObject) methodSignatureForSelector: after, will once again into the dynamic resolution

Note: Please refer to the problem exploration at the end of the article for detailed analysis process

Class method

For class methods, similar to instance methods, we can also solve the crash problem by overriding the resolveClassMethod class method in LGPerson and pointing the implementation of the sayNB class method to lgClassMethod

+ (BOOL)resolveClassMethod:(SEL) SEL {if (SEL == @selector(sayNB)) {NSLog(@"%@ ", NSStringFromSelector(SEL)); IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod)); Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod)); const char *type = method_getTypeEncoding(lgClassMethod); return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type); } return [super resolveClassMethod:sel]; }Copy the code

The CLS passed in is no longer a class, but a metaclass. You can get the metaclass of the class through the objc_getMetaClass method, because the class methods are instance methods in the metaclass

To optimize the

Is there a better way to do this once and for all? In fact, it can be found that there are two search paths through the method slow search process

  • Instance methods:Class -- parent class -- root class -- nil
  • Methods:Metaclass -- root metaclass -- root class -- nil

What they all have in common is if they don’t find it, they all go to the root class which is NSObject, so can we integrate these two methods together? The answer is yes, you can do this by adding classes to NSObject, and because class methods look up, in their inheritance chain, also look up instance methods, you can put the unified handling of instance methods and class methods in the resolveInstanceMethod method, as shown below

+ (BOOL)resolveInstanceMethod (SEL) SEL {if (SEL == @selector(say666)) {NSLog(@"%@ ", NSStringFromSelector(SEL)); IMP imp = class_getMethodImplementation(self, @selector(sayMaster)); Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster)); const char *type = method_getTypeEncoding(sayMethod); return class_addMethod(self, sel, imp, type); }else if (sel == @selector(sayNB)) {NSLog(@"%@ ", NSStringFromSelector(sel)); IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod)); Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod)); const char *type = method_getTypeEncoding(lgClassMethod); return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type); } return NO; }Copy the code

The implementation of this way, just with the source code for the method of class processing logic is consistent, that is, the perfect explanation why to call the method of class dynamic method resolution, but also call the object method dynamic method resolution, the fundamental reason is the method of class in the metaclass instance method.

Above this kind of writing, of course, there will be other problems, such as system method also can be changed, for this, can be optimized, namely we can according to the custom class method unified method name prefix, judging by the prefix is a custom method, and then unified handling custom methods, for example can pop before collapse to the home page, It is mainly used for anti-crash processing of APP online to improve user experience.

forward

In the slow process to find out, we know that if the fast + slow didn’t find method, dynamic method resolution also not line, using the message forwarding, however, we haven’t found searched source message forwarding relevant source, can through the following ways to get to know, go which methods before collapse method calls

  • throughinstrumentObjcMessageSendsMode Displays logs about sending messages
  • throughHopper/IDA decompiling

Through instrumentObjcMessageSends

  • Through lookUpImpOrForward – > log_and_fill_cache – > logMessageSend, found at the bottom of the logMessageSend source instrumentObjcMessageSends source code to achieve, so, In the main call instrumentObjcMessageSends print method call log information, has the following two preparations

    • 1. Open theobjcMsgLogEnabledSwitch, that is, callinstrumentObjcMessageSendsMethod when passed inYES
    • 2, inmainThrough theexternThe statementinstrumentObjcMessageSendsmethods
extern void instrumentObjcMessageSends(BOOL flag); int main(int argc, const char * argv[]) { @autoreleasepool { LGPerson *person = [LGPerson alloc]; instrumentObjcMessageSends(YES); [person sayHello]; instrumentObjcMessageSends(NO); NSLog(@"Hello, World!" ); } return 0; }Copy the code
  • throughlogMessageSendSource code, read the message sent to print information stored in/tmp/msgSendsTable of contents, as shown below \

  • Run the code and go to/tmp/msgSendsCatalog, found theremsgSendsAt the beginning of the log file, opening it found that before crashing, the following methods were executed
    • Two dynamic method resolutions: the resolveInstanceMethod method

    • Two news fast forwarding: forwardingTargetForSelector method

    • Two news slowly forward: methodSignatureForSelector + resolveInvocation

Decompilation via Hopper /IDA

Hopper and IDA are tools that help us statically analyze visibility files, disassemble executable files into pseudocode, control flow diagrams, etc. Take Hopper as an example. (Note: Hopper Advanced is a paid software, and the demo is sufficient for simpler disassembly needs.)

  • Run program crash, view stack information

  • found___forwarding___fromCoreFoundation\

  • throughimage list, read the entire image file, and then searchCoreFoundationTo view the path of its executable file \

  • Go to the file pathCoreFoundationtheExecutable file\

  • Open thehopper, the choice ofTry the DemoThen drag the executable from the previous step into Hopper for disassembly and selectx86(64 bits)\

  • The following is the interface after disassembly, mainly using the above three functions, respectively assembly, flow chart, pseudocode

  • Search for __forwarding_prep_0___ through the search box on the left, then select the pseudocode

    • The following is a__forwarding_prep_0___Assembler pseudocode, jump to___forwarding___

    • The following is a___forwarding___Pseudo-code implementation, the first is to see if it is implementedforwardingTargetForSelectorIf there is no response, jump toloc_6459bThat is, fast forwarding does not respond, enterSlowly forwardThe process,

    • Jump toloc_6459bBelow it to determine whether to respondmethodSignatureForSelectorMethod,

    • ifThere is no responseJump toloc_6490b, an error message is displayed
    • If you getmethodSignatureForSelectortheThe method signatureNil, which is also an error

    • ifmethodSignatureForSelectorIf the return value is not empty, theforwardInvocationMethod forinvocationFor processing

Therefore, through the above two search methods can be verified, there are three methods of message forwarding

  • Fast forward forwardingTargetForSelector 】

  • [Slow forwarding]

    • methodSignatureForSelector
    • forwardInvocation

Therefore, in summary, the overall process of message forwarding is as follows

The processing of message forwarding is divided into two parts:

  • “Fast forward” when slow lookup, resolution and dynamic methods were not find implementation, for message forwarding, the first is a quick message forwarding, namely forwardingTargetForSelector method

    • If the returnMessage receiverIf it is still not found in the message receiver, it enters the search process of another method
    • If the returnnil“, the slow message forwarding is displayed
  • Perform to slow forward 】 【 methodSignatureForSelector method

    • If the returnedThe method signaturefornilThe directCollapse of the error
    • If the returned method is signedDon't is nilAnd went toforwardInvocationMethod, the Invocation transaction is handled and no error is reported if it is not

Second chance: Quick retweet

According to the collapse of the above problem, if the dynamic method resolution but could not find, you need to rewrite forwardingTargetForSelector method in LGPerson, Specify the recipient of LGPerson’s instance method as LGStudent’s object (LGStudent class has a concrete implementation of Say666), as shown below

- (id)forwardingTargetForSelector:(SEL)aSelector{ NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector)); Return [LGStudent alloc]; return [LGStudent alloc]; }Copy the code

The result is as follows:You can also specify no message receiver,Call the method directly from the parent class, if still not found,Direct error

[Third chance] Slow forwarding

For a second chance in fast forward or not found, is in the final of a saving opportunity, namely in the LGPerson rewrite methodSignatureForSelector, as shown below:

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

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

The print result is as follows, foundforwardInvocationThe Method does not handle the invocation and does not crash

Can also beHandling invocation transactions, as shown below, modifyinvocationthetargetfor[LGStudent alloc], the call[anInvocation invoke]To trigger theLGPersonOf the classsay666The invocation of the instance method is calledLGStudentthesay666methods

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    anInvocation.target = [LGStudent alloc];
    [anInvocation invoke];
}
Copy the code

The print result is as follows:Therefore, from the above, no matter inforwardInvocationIn the methodWhether to handle the InvocationTransactions, programsWon't break down.

“Why are dynamic method resolutions executed twice?” To explore problems

The dynamic method resolution method mentioned in the previous article has been implemented twice, with the following two ways of analysis

Explore god’s perspectiveIn the slow search process, we learned thatresolveInstanceMethodMethod is executed throughlookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethodCame toresolveInstanceMethodSource code, in the source code by sendingresolve_selMessage firing, as shown below

  • IMP IMP = lookUpImpOrNil(inST, SEL, CLS); Add a breakpoint, print the stack through Bt and see what’s going on

  • inresolveInstanceMethodIn the methodIMP imp = lookUpImpOrNil(inst, sel, cls);Add a breakpoint and run the program until the first time"Come", using BTFirst dynamic method resolutionIs the stack information of selsay666

  • Continue until the second time"Coming" print, looking at the stack information, in the second, we can see that is throughCoreFoundationthe-[NSObject(NSObject) methodSignatureForSelector:]Method, and then throughclass_getInstanceMethodGoing into dynamic method resolution again,

  • So with the stack information from the last step, we need to look at what’s going on in CoreFoundation? throughHopperThe disassemblyCoreFoundationThe executable file to viewmethodSignatureForSelectorMethod pseudocode

  • throughmethodSignatureForSelectorPseudocode entry___methodDescriptionForSelectorThe implementation of the

  • Enter the___methodDescriptionForSelectorThe pseudo-code implementation, combined with the assembled stack print, can be seen in___methodDescriptionForSelectorThat’s called in this methodobjc4-781theclass_getInstanceMethod

  • Search the source code in objCclass_getInstanceMethod, the source code implementation is shown below

This point can be passedCode debuggingTo verify, as shown below, inclass_getInstanceMethodAdd a breakpoint to the method and executemethodSignatureForSelectorAfter the method, the signature is returned, indicating that the method signature is in effect, and apple is walking toinvocationBefore,Gives developers a chance to query againSo go toclass_getInstanceMethodHere, go to the method query againsay666And then I’ll go toDynamic method resolution

Therefore, the above analysis also confirms the previous articleresolveInstanceMethodMethod executed twice.

Exploration without a God’s perspective

If there is no God perspective, we can also use the code to deduce where the dynamic method resolution is called again

  • In the LGPerson rewriteresolveInstanceMethodMethod, and plusclass_addMethodOperating theAssignment IMPAt this time,resolveInstanceMethodWill you go twice?

    【 Conclusion 】 : Through the run found, if the value of IMP, dynamic method resolution will only walk once, indicating that it is not here to walk the second dynamic method resolution,

Keep exploring

  • To get rid ofresolveInstanceMethodMethod to assign IMP, inLGPersonClass to rewriteforwardingTargetForSelectorMethod and specify that the return value is[LGStudent alloc], rerun ifresolveInstanceMethodIt was printed twice, which means it wasforwardingTargetForSelectorMethod executes the dynamic method resolution before, and vice versaforwardingTargetForSelectorMethods after[Conclusion] : DiscoveryresolveInstanceMethodThe print is still only printed once, ranking the second dynamic method resolution inforwardingTargetForSelectorMethods after
  • I’ll rewrite it in LGPersonmethodSignatureForSelector 和 forwardInvocationRun,

    [Conclusion] :Second dynamic method resolutioninmethodSignatureForSelector 和 forwardInvocationMethods between

The second analysis also demonstrates why the resolveInstanceMethod was executed twice

After the above argument, we know that in fact, in the slow boy forwarding process, inmethodSignatureForSelectorforwardInvocationThere is also a dynamic method resolution between methods, which Apple gives a chance to do again, as shown in the figure below

conclusion

So far, the objc_msgSend message sending process has been analyzed and summarized here

  • [Quick search process]First, in the classBuffer cacheTo find the implementation of the specified method
  • [Slow search process]If it is not found in the cache, theClass method listIf still not found, go toA list of caches and methods for the parent class chainLook for
  • [Dynamic Method resolution]If a slow search doesn’t find anything,First chance to fix itJust try it onceDynamic method resolution, that is, to rewrite theresolveInstanceMethod/resolveClassMethodmethods
  • [Message forwarding]If the dynamic method resolution is still not found, proceedforwardThere are two remedial opportunities in message forwarding:Fast forwarding + Slow forwarding
  • If not, the program will crashunrecognized selector sent to instance

The article lists

List of blog posts

reference

IOS – Underlying Principles 14: Dynamic method resolution for Message flow analysis & message forwarding