primers

In the previous two articles objc_msgSend Process analysis Fast Lookup and objc_msgSend process analysis Slow lookup, apple analyzed fast lookup and slow lookup of objc_msgSend respectively. In the case of failing to find a way to implement either method, Apple gave two suggestions

  • Dynamic method resolutionIf the slow lookup process is not found, a dynamic method resolution is executed
  • 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 unimplemented crash of our common methods in daily development, as follows

  • Define LGPerson class where neither say666 instance method nor sayNB class method is implemented

  • Call LGPerson instance say666 and class sayNB respectively in main, and run the program, both of which will raise an error indicating that the method is not implemented, as shown below

    • Error result from calling instance method say666

    • Error result from calling class method sayNB

Method not implemented error source code

According to the source code of a slow search, we found that the error is finally to the __objc_msgForward_impcache method, the following is the source of the error process

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
  • Assembler implementation to find__objc_forward_handler, and did not find, in the source code to remove an underscore for global search_objc_forward_handler, has the following implementation, which is essentially calledobjc_defaultForwardHandlermethods
// 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)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

Copy the code

If objc_defaultForwardHandler looks familiar, this is the most common error we see in everyday development: not implementing a function, running an application, crashing.

Now, let’s talk about how to prevent unimplemented crashes of methods before they crash.

Three method lookups to save the opportunity

Based on apple’s two suggestions, we have three salvage opportunities:

  • First chance dynamic method resolution

  • Message Forwarding Process

    • 【 SECOND chance 】Fast forward
    • 【 Third Chance 】Slowly forward

First chance dynamic method resolution

In the slow search process did not find the method implementation, first will try a dynamic method resolution, its source code implementation is as follows:

static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertLocked(); ASSERT(cls->isRealized()); runtimeLock.unlock(); // object -- class if (! CLS - > isMetaClass ()) {/ / class is not a metaclass, call the object's parsing methods / / try/CLS resolveInstanceMethod: sel resolveInstanceMethod (inst, sel, CLS); } else {// If it is a metaclass, call the class's parsing method, Classes - yuan / / 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 we still need to query the dynamic method resolution of object methods in the metaclass. LookUpImpOrNil (inst, sel, CLS) {// If not found or empty, look for resolveInstanceMethod(inst, sel, CLS) in the object method resolution method of the metaclass; // chances are that calling the resolver have populated the cache // so attempt using it To find out the process continued to walk return lookUpImpOrForward (inst, sel, CLS, behaviors | LOOKUP_CACHE); }Copy the code

It is mainly divided into the following steps

  • 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 in a metaclassCould not findOr foremptyWhile the,The metaclasstheInstance methodsDynamic method resolutionresolveInstanceMethodIn the search, mainly becauseClass methods are instance methods in metaclasses, so you also need to look for dynamic method resolutions for instance methods in the metaclass
  • ifDynamic method resolution, itThe implementation points to other methods, then continueFinds the specified IMP, that is, continue the slow searchlookUpImpOrForwardprocess

The process is as follows

Instance methods

ResolveInstanceMethod resolveInstanceMethod resolveInstanceMethod resolveInstanceMethod resolveInstanceMethod resolveInstanceMethod resolveInstanceMethod resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); SEL resolve_sel = @selector(resolveInstanceMethod:); // look is resolveInstanceMethod -- 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); // Cache the result (good or bad) so the resolver doesn't fire next time. // resolveInstanceMethod Adds to self A.K.A. CLS // find SAY666 IMP IMP = lookUpImpOrNil(INST, SEL, CLS); 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)); }}}Copy the code

It is mainly divided into the following steps:

  • inSend a resolveInstanceMethod messageBefore, you need to look for CLSclassWhether there is this methodimplementation, that is, throughlookUpImpOrNilMethods will come in againlookUpImpOrForwardSlow search process searchresolveInstanceMethodmethods
    • If not, return directly
    • If yes, send itresolveInstanceMethodThe message
  • Again slowly find the implementation of the instance method, that is, throughlookUpImpOrNilMethods will come in againlookUpImpOrForwardSlow search process searchInstance methods

Collapse to modify

For instance say666 to fail, we can override the resolveInstanceMethod method in LGPerson and point it to another method. 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 the sayMaster method imp imp imp = class_getMethodImplementation(self, @selector(sayMaster)); SayMethod = class_getInstanceMethod(self, @selector(sayMaster)); Const char *type = method_getTypeEncoding(sayMethod); SayMaster return class_addMethod(self, sel, imp, type); } return [super resolveInstanceMethod:sel]; }Copy the code

Run it again, and the result is printed as follows

The resolveInstanceMethod dynamic resolution method prints “come” twice. You can see this by looking at the stack information

  • [first dynamic resolution] The first “coming” is in searchsay666Method will enterDynamic method resolution
  • [Second dynamic resolution] The second “coming” is called in the slow forwarding processCoreFoundationIn the frameworkNSObject(NSObject) methodSignatureForSelector:After, will againEnter 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.

Message Forwarding Process

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

  • Print by instrumentObjcMessageSends sends the message log

  • Decompilation via Hopper /IDA

Through instrumentObjcMessageSends

  • throughlookUpImpOrForward --> log_and_fill_cache --> logMessageSend, found under the logMessageSend source codeinstrumentObjcMessageSendsSource code implementation, so, call in maininstrumentObjcMessageSendsTo print log information about method calls, the following two preparations are required
    • 1, open the objcMsgLogEnabled switch, namely call instrumentObjcMessageSends method, introduced to YES

    • 2, in the main by extern declaration instrumentObjcMessageSends method

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
  • The logMessageSend source code shows that the sent messages are stored in the/TMP /msgSends directory, as shown below

  • Run the code, go to the/TMP /msgSends directory, find a log file that starts with msgSends, open it, and execute the following method before crashing

    • Two dynamic method resolutions: the resolveInstanceMethod method

    • Two news fast forwarding: forwardingTargetForSelector method

    • Two news slowly forward: methodSignatureForSelector + resolveInstanceMethod

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___ from CoreFoundation

  • Through the Image List, read the entire image file, and then search for CoreFoundation to see the path to its executable

  • Follow the file path to the CoreFoundation executable

  • Open hopper, select Try the Demo, then drag the executable from the previous step into Hopper for disassembly, select x86(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

    • Here is assembly pseudocode for __forwarding_prep_0___, jumping to ___forwarding___

    • Here is ___forwarding___ pseudo code realization, first is to look at whether forwardingTargetForSelector method, if there is no response, jump to loc_6459b namely fast forwarding no response, forward into a slow process,

    • Jump to loc_6459b, below determine whether response methodSignatureForSelector method,

      • If there is no response, jump to loc_6490b, an error is reported

      • If get methodSignatureForSelector method signature is nil, but also an error directly

  • If methodSignatureForSelector the return value is not null, then in forwardInvocation approach to deal with invocation

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

  • [Fast forwarding]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 message receiver is returned and still not found in the message receiver, the look-up process for another method is entered

    • If nil is returned, slow message forwarding is entered

  • Perform to slow forward 】 【 methodSignatureForSelector method

    • If the method returned is signed nil, it crashes

    • If the returned method is not signed nil, go to the forwardInvocation method and the invocation transaction is handled without an error

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 call the method of the parent class without specifying the receiver. If no message is found, an error is reported

[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 forwardInvocation method does not handle the invocation and does not crash

It is also possible to handle the Invocation transaction, as shown below, by modifying the Invocation target to [LGStudent alloc], The invocation of LGPerson say666 will invoke LGStudent say666

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

Copy the code

The print result is as follows

So, from the above, the program does not crash whether the forwardInvocation method handles the invocation transaction or not.

“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 perspective

In the slow lookup process, The resolveInstanceMethod method is executed via lookUpImpOrForward –> resolveMethod_locked –> ResolveInstanceMethod goes to the resolveInstanceMethod source code, which is triggered by sending the resolve_sel message, as shown below

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

  • IMP IMP = lookUpImpOrNil(inST, SEL, CLS) in resolveInstanceMethod Add a breakpoint, run the program until the first “coming”, through bt to see the stack information of the first dynamic method resolution, sel is say666

  • Continue to execute down until the second “to the” print, view the stack information, in the second, we can see through CoreFoundation – [NSObject (NSObject) methodSignatureForSelector:] approach, Then enter the dynamic method resolution again with class_getInstanceMethod,

  • So with the stack information from the last step, we need to look at what’s going on in CoreFoundation? Through the Hopper of the disassembly CoreFoundation executable file, see methodSignatureForSelector method of pseudo code

  • By entering ___methodDescriptionForSelector methodSignatureForSelector pseudo code implementation

  • Enter ___methodDescriptionForSelector pseudo code realization, combine compiled stack print, you can see, In ___methodDescriptionForSelector this method calls the class_getInstanceMethod objc4-781

  • Search for class_getInstanceMethod in the source code in objC, whose source code implementation is shown below

This can be through code debugging to verify, as shown below, add a breakpoint at class_getInstanceMethod method, after execution methodSignatureForSelector method, returned to the signature, illustrate the method signature is effective, Apple gives the developer a chance to invocation again before the Invocation goes to class_getInstanceMethod, the method query say666 again, and then the dynamic method resolution again

Therefore, the above analysis also confirms the reason why the resolveInstanceMethod method was 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

  • LGPerson () : IMP (class_addMethod) : IMP (class_addMethod)

    【 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

  • Remove the assignment the resolveInstanceMethod method IMP in LGPerson rewrite forwardingTargetForSelector method in class, and specify the return value is [LGStudent alloc], to run, If the resolveInstanceMethod print twice, that is before forwardingTargetForSelector method performs a dynamic method resolution, on the other hand, after forwardingTargetForSelector method

    【 conclusion 】 : resolveInstanceMethod found in the print or print only once; number ranked second resolution after forwardingTargetForSelector method dynamic method

  • In LGPerson rewrite methodSignatureForSelector and forwardInvocation, run

    【 conclusion 】 : the second dynamic method resolution between methodSignatureForSelector and forwardInvocation method

The second analysis also demonstrates why the resolveInstanceMethod was executed twice

Through the above argument, we know that in fact in a slow boy forward process, between methodSignatureForSelector and forwardInvocation method and a dynamic method for resolution, the apple to give a chance again, as shown in the figure below

conclusion

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

  • First, find the implementation of the specified method in the class’s cache cache

  • If it is not found in the cache, it is searched in the method list of the class. If it is still not found, it is searched in the cache and method list of the parent class chain

  • If slow dynamic method resolution 】 【 find or not find, first chances resolution is to try a dynamic method, namely rewrite resolveInstanceMethod/resolveClassMethod method

  • [Message forwarding] If the dynamic method resolution is still not found, the message is forwarded, and there are two remedial opportunities in message forwarding: fast forwarding + slow forwarding

  • If there is no such error after forwarding, the program is sent to instance