(Summary of articles on underlying principles of iOS)
(iOS)
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 executedforward
: 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
- define
LGPerson
Class,say666
Instance methods andsayNB
Class 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 codeCopy the code
- In the assembly implementation
__objc_forward_handler
Is not found, remove an underscore from the source code to search globally_objc_forward_handler
, has the following implementation, which is essentially calledobjc_defaultForwardHandler
methods
// 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 codeCopy 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
- 【 Second chance 】
[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 codeCopy the code
Mainly divided into the following steps
-
Determine whether the class is a metaclass
- If it is
class
, the implementation ofInstance methods
Dynamic method resolutionresolveInstanceMethod
- If it is
The metaclass
, the implementation ofClass method
Dynamic method resolutionresolveClassMethod
If in a metaclassCould not find
Or forempty
While the,The metaclass
theInstance methods
Dynamic method resolutionresolveInstanceMethod
In, 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
- If it is
-
If the dynamic method resolution points its implementation to another method, the search for the specified IMP continues, that is, the slow search for the lookUpImpOrForward process continues
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 codeCopy the code
The main steps are as follows:
-
Before sending the resolveInstanceMethod message, we need to find whether the CLS class has the implementation of the method, that is, through the lookUpImpOrNil method and then enter the lookUpImpOrForward slow search process to find the resolveInstanceMethod method
- If not, it is returned directly
- If yes, send it
resolveInstanceMethod
The message
-
The implementation of slow lookup instance method again, that is, through the lookUpImpOrNil method will enter the lookUpImpOrForward slow lookup process to find the instance method
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 codeCopy the code
Run it again, and the print is as follows
As you can see from the results,resolveInstanceMethod
The “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 search
say666
Method will enterDynamic method resolution
- [Second dynamic resolution] The second “coming” is called in the slow forwarding process
CoreFoundation
In 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 codeCopy 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 codeCopy 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
- through
instrumentObjcMessageSends
Logs about sending messages are displayed - through
Hopper/IDA decompiling
Through instrumentObjcMessageSends
- through
lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
, found in the logMessageSend source belowinstrumentObjcMessageSends
So, 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 ` methods duplicated codeCopy 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 codeCopy the code
- through
logMessageSend
Source code that understands the message sent to print information stored in/tmp/msgSends
Directory, as shown below
-
Run the code and go to the/TMP /msgSends directory, find a log file starting with msgSends, open it and find that the following method was executed before the crash
- two
Dynamic method resolution
:resolveInstanceMethod
methods - two
Fast message forwarding
:forwardingTargetForSelector
methods - two
Message forwarding is slow
:methodSignatureForSelector
+resolveInvocation
- two
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
- through
image list
, read the entire image file, and then searchCoreFoundation
To view the path of its executable file
- Through the file path, find
CoreFoundation
theExecutable file
- Open the
hopper
, 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 implementationforwardingTargetForSelector
Method, if there is no response, jump toloc_6459b
That is, fast forwarding does not respond, enterSlowly forward
The process,
- Jump to
loc_6459b
To determine whether to respondmethodSignatureForSelector
Method,
– if
There is no response
Jump 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 codeCopy the code
- The following is a
-
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 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 return
Message receiver
, is still not found in the message receiver, then it enters the lookup process of another method - If the return
nil
, the slow message forwarding is enabled
- If the return
-
Perform to slow forward 】 【 methodSignatureForSelector method
- If the return
The method signature
fornil
The directCollapse of the error
- If the method signature is returned
Don't is nil
And went toforwardInvocation
Method, the invocation transaction is processed, and if it is not processed, it will not be reported
- If the return
[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 codeCopy the code
The execution result is as follows
You can also simply not specify a message receiver,Call this method directly on the parent class
If 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 codeCopy the code
The print result is as follows, foundforwardInvocation
There 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 codeCopy 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 thatresolveInstanceMethod
The execution of the method is bylookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod
Came toresolveInstanceMethod
Source code, by sending in source coderesolve_sel
Message is triggered, as shown below
(int, sel, CLS); (int, sel, CLS); Put a breakpoint at, print the stack through BT and see what happened
- in
resolveInstanceMethod
In 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 resolution
In this case, sel issay666
- Keep going until the second time
"Here comes" print
To view the stack information, in the second case, we can see that is passedCoreFoundation
the-[NSObject(NSObject) methodSignatureForSelector:]
Method and then throughclass_getInstanceMethod
Going 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. through
Hopper
The disassemblyCoreFoundation
Executable file, viewmethodSignatureForSelector
Method
- through
methodSignatureForSelector
Pseudocode entry___methodDescriptionForSelector
The implementation of the
- Enter the
___methodDescriptionForSelector
The pseudo code implementation, combined with the assembly stack print, can be seen in___methodDescriptionForSelector
It’s called in this methodobjc4-781
theclass_getInstanceMethod
- Search in the source code in objC
class_getInstanceMethod
, whose source implementation is shown below
This point can be passedCode debugging
To verify, as shown below, inclass_getInstanceMethod
Add a breakpoint to the method, and then executemethodSignatureForSelector
Method, return the signature, indicating that the method signature is in effect, apple in the walk toinvocation
Before,It gives developers a chance to look it up again
“, so walk toclass_getInstanceMethod
Here, we go through the method query againsay666
And 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 rewrite
resolveInstanceMethod
Method, and addclass_addMethod
Operating theAssignment IMP
At this time,resolveInstanceMethod
Will 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 of
resolveInstanceMethod
Method assignment IMP, inLGPerson
Class to rewriteforwardingTargetForSelector
Method and specify the return value as[LGStudent alloc]
, rerun ifresolveInstanceMethod
I printed it twice. It says it’s inforwardingTargetForSelector
Method before executing a dynamic method resolution, and vice versa, inforwardingTargetForSelector
Methods after
【 Conclusion 】 : DiscoveryresolveInstanceMethod
The print is still only printed once, and the number ranks the second dynamic method resolution inforwardingTargetForSelector
Methods after
- Rewrite it in LGPerson
methodSignatureForSelector
和forwardInvocation
Run,
【 Conclusion 】 :Second dynamic method resolution
inmethodSignatureForSelector
和 forwardInvocation
Methods between
The second analysis also demonstrates why resolveInstanceMethod was executed twice
After the above demonstration, we know that in the slow kid forwarding process, inmethodSignatureForSelector
和 forwardInvocation
There 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 search process]
First, in the classBuffer cache
To find the implementation of the specified method[Slow search process]
If not found in the cache, inClass
If you still can’t find it, goThe cache and method list of the superclass chain
Look for[Dynamic method resolution]
If the slow search still doesn’t find it,The first chance to make up for it
Just try it onceDynamic method resolution
, that is, to rewrite theresolveInstanceMethod
/resolveClassMethod
methods[Message forwarding]
If the dynamic method resolution is still not found, proceedforward
, there are two remedial opportunities in message forwarding:Fast forwarding and slow forwarding
- If there is no after forwarding, the program directly reported an error crash
unrecognized selector sent to instance