IOS underlying principles + reverse article summary

primers

In the previous two articles, iOS- Underlying Principle 12: Fast search for Message Flow Analysis and iOS- Underlying Principle 13: Slow Search for Message Flow Analysis, the fast search and slow search for objc_msgSend were analyzed respectively. In the case of neither method was found, Apple gave two suggestions

  • Dynamic method resolution: If the slow search process is not found, a dynamic method resolution will be executed
  • forward: If the dynamic method resolution still does not find an implementation, the message is forwarded

If neither of these suggestions is done, we will report an unimplemented crash, which is common in our daily development, as follows

  • defineLGPersonClass,say666Instance methods andsayNBClass methods areDo not implement

  • In main, LGPerson instance method say666 and class method sayNB are called respectively. When the program is run, error will be reported, 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

According to the slow search source code, we found that the error finally goes to the __objc_msgForward_impcache method, the following is the source code 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
  • In the assembly implementation__objc_forward_handlerIs not found, remove an underscore from the source code to search globally_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, it’s one of the most common mistakes we make in everyday development: failing to implement a function, running a program, and crashing.

Now, let’s talk about how to do something before it crashes to prevent unimplemented crashes.

Three ways to find the chance to save

Based on Apple’s two suggestions, we have three chances to save the day:

  • [First opportunity] Dynamic method resolution

  • Message forwarding process

    • 【 Second chance 】Fast forward
    • 【 Third chance 】Slowly forward

[First opportunity] Dynamic method resolution

When the slow search process does not find the method implementation, it will first 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 resolution method of the class, 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 metaclass's object method resolution method; } // chances are that calling the resolver have populated the cache // so attempt to use it // If the method resolves to point to another method, To find out the process continued to walk return lookUpImpOrForward (inst, sel, CLS, behaviors | LOOKUP_CACHE); }Copy the code

Mainly divided into the following steps

  • Determine whether the class is a metaclass
    • If it isclass, the implementation ofInstance methodsDynamic method resolutionresolveInstanceMethod
    • If it isThe metaclass, the implementation ofClass methodDynamic method resolutionresolveClassMethodIf in a metaclassCould not findOr foremptyWhile the,The metaclasstheInstance methodsDynamic method resolutionresolveInstanceMethodIn, mainly becauseClass methods are instance methods in a metaclass, so you also need to find the dynamic method resolution of the instance method in the metaclass
  • ifDynamic method resolution, itThe implementation points to other methods, then continueFind the specified IMP, that is, continue the slow searchlookUpImpOrForwardprocess

The process is as follows

Instance methods

For instance method invocation, in the fast – slow search did not find the implementation of the instance method, we have a chance to save, that is, try a dynamic method resolution, because it is an instance method, so it will go to the resolveInstanceMethod method, its source code is as follows

static void resolveInstanceMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); SEL resolve_sel = @selector(resolveInstanceMethod:); // Look at resolveInstanceMethod -- this is the fault tolerance before sending the message 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 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

The main steps are as follows:

  • inSends a resolveInstanceMethod messageBefore, you need to look for CLSclassIs there a method in theimplementation, that is, throughlookUpImpOrNilThe method will enter againlookUpImpOrForwardSlow search Process searchresolveInstanceMethodmethods
    • If not, it is returned directly
    • If yes, send itresolveInstanceMethodThe message
  • Again, slow down the implementation of the instance method, that is, throughlookUpImpOrNilThe method will enter againlookUpImpOrForwardSlow search Process searchInstance methods

Collapse to modify

So, if the instance method say666 fails to implement a crash, you can override the resolveInstanceMethod method in your class and point it to the implementation of another method, i.e. override the resolveInstanceMethod method in LGPerson, Point the implementation of the instance method say666 to the sayMaster method implementation, as shown below

+ (BOOL)resolveInstanceMethod:(SEL) SEL {if (SEL == @selector(say666)) {NSLog(@"% @selector ", NSStringFromSelector(SEL)); // Get imp IMP 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); } return [super resolveInstanceMethod:sel]; }Copy the code

Run it again, and the print is as follows

As you can see from the results,resolveInstanceMethodThe “coming” is printed twice in the dynamic resolution method. Why? This can be seen from the stack information

  • [the first dynamic resolution] The first “coming” is in the 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: For a detailed analysis process, see the question exploration at the end of the paper

Class method

For class methods, similar to instance methods, you can also override the resolveClassMethod class method by overriding it in class LGPerson and referring the implementation of the sayNB class method to class method lgClassMethod

+ (BOOL)resolveClassMethod:(SEL) SEL {if (SEL == @selector(sayNB)) {NSLog(@"% @selector(SEL) ", 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

Rewriting resolveClassMethod class methods need to note that the incoming CLS is no longer a class, but a metaclass, can be obtained through objc_getMetaClass metaclass, because class methods is the instance method in the metaclass

To optimize the

The above method is overridden separately in each class, is there a better, once and for all method? In fact, it can be found that there are two search paths through the method slow search process

  • Example method:Class -- parent -- root -- nil
  • Methods:Metaclass -- root metaclass -- root class -- nil

What they all have in common is that if they didn’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, by adding classes to NSObject, and because class methods look for instance methods in their inheritance chain, you can put the unified processing of instance methods and class methods in the resolveInstanceMethod method, as shown below

+ (BOOL)resolveInstanceMethod:(SEL) SEL {if (SEL == @selector(say666)) {NSLog(@"% @selector ", 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(@"%@ coming ", 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

This way of implementation, just with the source code for the class method processing logic is consistent, that is, the perfect explanation of why the call class method dynamic method resolution, but also call the object method dynamic method resolution, the root cause of the class method 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 and 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

  • Decompile by Hopper /IDA

Through instrumentObjcMessageSends

  • throughlookUpImpOrForward --> log_and_fill_cache --> logMessageSend, found in the logMessageSend source belowinstrumentObjcMessageSendsSo, call it in main

InstrumentObjcMessageSends print method call log information, has the following two preparations – 1, open objcMsgLogEnabled switch, is called instrumentObjcMessageSends method, introduced to YES

- 2, in ` main ` by ` extern ` statement ` instrumentObjcMessageSends ` methodCopy the code
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 that understands the message sent to print information stored in/tmp/msgSendsDirectory, as shown below

  • Run the code and go/tmp/msgSendsDirectory, foundmsgSendsOpen the log file at the beginning and discover that the following method was performed before the crash
    • twoDynamic method resolution:resolveInstanceMethodmethods
    • twoFast message forwarding:forwardingTargetForSelectormethods
    • twoMessage forwarding is slow:methodSignatureForSelector + resolveInvocation

Decompile by Hopper /IDA

Hopper and IDA are tools for static analysis of visible files. They can disassemble executable files into pseudocode, control flow charts, etc. Hopper is an example.

  • 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

  • Through the file path, findCoreFoundationtheExecutable file

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

  • The following is the interface after disassembly, mainly using the above three functions, namely assembly, flow chart, pseudo code

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

    • The following is a__forwarding_prep_0___Assembler pseudocode to jump to___forwarding___

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

    • Jump toloc_6459bTo determine whether to respondmethodSignatureForSelectorMethod,

    – ifThere is no responseJump toloc_6490b, an error is directly reported

    - if you get ` methodSignatureForSelector ` ` method signature ` is nil, is also a direct error! [pseudo code - methodSignatureForSelector is nil times wrong] (HTTP: / / https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/462f6bcae13041c3afdbe5db4d551f c6~tplv-k3u1fbpfcp-zoom-1.image)Copy the code
  • If methodSignatureForSelector the return value is not null, then in forwardInvocation approach to deal with invocation

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

  • [Fast forward]forwardingTargetForSelector
  • [Slow forwarding]
    • methodSignatureForSelector
    • forwardInvocation

To sum up, the overall process of message forwarding is as followsMessage forwarding is mainly divided into two parts:

  • [Fast forwarding] When the slow lookup and dynamic method resolution are not found, the message forwarding is carried out firstQuick message forwarding, that is, go toforwardingTargetForSelectormethods
    • If the message receiver is returned and it is still not found in the message receiver, it enters the lookup process of another method

    • If nil is returned, then slow message forwarding is entered

  • [Slow Forwarding] Executes tomethodSignatureForSelectormethods
    • If the method is returned with a nil signature, it crashes

    • If the returned method signature is not nil, go to the Fore Invocation method and handle the transaction, and it won’t happen if it’s not handled

[Second chance] fast forward

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 receiver of the instance method of LGPerson as an object of LGStudent (the LGStudent class has a concrete implementation of Say666), as shown below

- (id)forwardingTargetForSelector:(SEL)aSelector{ NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector)); // Runtime + aSelector + addMethod + IMP // Specify the receiver of the message as LGStudent and look for the implementation of say666 return [LGStudent alloc]; }Copy the code

The execution result is as follows

You can also simply not specify a message receiver,Call this method directly on the parent classIf it is not found, thenDirect 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, foundforwardInvocationThere is no invocation and no breakdown

You can also handle the Invocation transaction, as shown below, by changing the Target to [LGStudent alloc], The LGStudent SAY666 invocation is invoked from the LGPerson class and is also invoked from the LGStudent SAY666 invocation

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

Copy the code

The print result is as follows

So, as you can see from the above, the program does not crash regardless of whether the Invocation is handled in the Fore Invocation method or not.

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

As mentioned in the previous article, the dynamic method resolution method is executed twice, and there are two ways to analyze it

Explore from the perspective of God

In the slow lookup process, we learned thatresolveInstanceMethodThe execution of the method is bylookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethodCame toresolveInstanceMethodSource code, by sending in source coderesolve_selMessage is triggered, as shown below

(int, sel, CLS); (int, sel, CLS); Put a breakpoint at, print the stack through BT and see what happened

  • inresolveInstanceMethodIn the methodIMP imp = lookUpImpOrNil(inst, sel, cls);Put a breakpoint at, and run the program until the first time"Come", and check through BTFirst dynamic method resolutionIn this case, sel issay666

  • Keep going until the second time"Here comes" printTo view the stack information, in the second case, we can see that is passedCoreFoundationthe-[NSObject(NSObject) methodSignatureForSelector:]Method and then throughclass_getInstanceMethodGoing back to dynamic method resolution,

  • With the stack information from the previous step, we need to look at what’s really going on in CoreFoundation. throughHopperThe disassemblyCoreFoundationExecutable file, viewmethodSignatureForSelectorMethod

  • throughmethodSignatureForSelectorPseudocode entry___methodDescriptionForSelectorThe implementation of the

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

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

This point can be passedCode debuggingTo verify, as shown below, inclass_getInstanceMethodAdd a breakpoint to the method, and then executemethodSignatureForSelectorMethod, return the signature, indicating that the method signature is in effect, apple in the walk toinvocationBefore,It gives developers a chance to look it up again“, so walk toclass_getInstanceMethodHere, we go through the method query againsay666And then you go back toDynamic method resolution

Therefore, the above analysis also confirms the reason why the resolveInstanceMethod method was executed twice in the previous article

Exploration from the perspective of no God

In the absence of god’s perspective, we can also derive in code where is the dynamic method resolution called again

  • In the LGPerson rewriteresolveInstanceMethodMethod, and addclass_addMethodOperating theAssignment IMPAt this time,resolveInstanceMethodWill it go twice?

[Conclusion] : Through the operation,If IMP is assigned, the dynamic method resolution only goes once, indicating that instead of going through the second dynamic method resolution here,

Keep exploring

  • To get rid ofresolveInstanceMethodMethod assignment IMP, inLGPersonClass to rewriteforwardingTargetForSelectorMethod and specify the return value as[LGStudent alloc], rerun ifresolveInstanceMethodI printed it twice. It says it’s inforwardingTargetForSelectorMethod before executing a dynamic method resolution, and vice versa, inforwardingTargetForSelectorMethods after

【 Conclusion 】 : DiscoveryresolveInstanceMethodThe print is still only printed once, and the number ranks the second dynamic method resolution inforwardingTargetForSelectorMethods after

  • Rewrite it in LGPersonmethodSignatureForSelectorforwardInvocationRun,

【 Conclusion 】 :Second dynamic method resolutioninmethodSignatureForSelectorforwardInvocationMethods between

The second analysis also demonstrates why resolveInstanceMethod was executed twice

After the above demonstration, we know that in the slow kid forwarding process, inmethodSignatureForSelectorforwardInvocationThere is also a dynamic method resolution between methods, which is another opportunity given by Apple, as shown in the figure below

conclusion

So far, the process of objc_msgSend sending messages has been analyzed, which is briefly summarized here

  • [Quick lookup process] First, the class cache looks for the implementation of the specified method

  • [Slow lookup process] If not found in the cache, then look in the class method list, if still not found, then look in the superclass chain cache and method list

  • 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 forwarding is carried out. There are two remedial opportunities in the message forwarding: fast forwarding + slow forwarding

  • If no selector is sent to instance after forwarding, the program crashes directly