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 givenforward
: 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 sayNB
andExample method sayHello
Error 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 is
Dynamic method resolution
- Class method +(BOOL)resolveClassMethod:(SEL) SEL;
- Example method + (BOOL)resolveInstanceMethod:(SEL) SEL;
- forward
- A second chance
Fast forward
- – (id) forwardingTargetForSelector (SEL) aSelector;
- Third chance
Slowly forward
- – (NSMethodSignature *) methodSignatureForSelector (SEL) aSelector;
- – (void) forwardInvocation (anInvocation NSInvocation *);
- A second chance
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 is
class
, the implementation ofInstance methods
Dynamic method resolutionresolveInstanceMethod
- If it is
The metaclass
, the implementation ofClass method
Dynamic method resolutionresolveClassMethod
- If you’re in a metaclass
Could not find
Or forempty
While the,The metaclass
theInstance methods
Dynamic method resolutionresolveInstanceMethod
, mainly because the metaclass’s recovery metaclass inheritance chain goes up toNSObject class
Would go,NSObject class
theInstance methods
.
- If you’re in a metaclass
- If it is
-
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 it
resolveInstanceMethod
The 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,resolveInstanceMethod
The “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
- through
instrumentObjcMessageSends
Mode Displays logs about sending messages - through
Hopper/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 the
objcMsgLogEnabled
Switch, that is, callinstrumentObjcMessageSends
Method when passed inYES
- 2, in
main
Through theextern
The statementinstrumentObjcMessageSends
methods
- 1. Open the
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
- through
logMessageSend
Source code, read the message sent to print information stored in/tmp/msgSends
Table of contents, as shown below \
- Run the code and go to
/tmp/msgSends
Catalog, found theremsgSends
At 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
\
- through
image list
, read the entire image file, and then searchCoreFoundation
To view the path of its executable file \
- Go to the file path
CoreFoundation
theExecutable file
\
- Open the
hopper
, the choice ofTry the Demo
Then 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 implementedforwardingTargetForSelector
If there is no response, jump toloc_6459b
That is, fast forwarding does not respond, enterSlowly forward
The process,
- Jump to
loc_6459b
Below it to determine whether to respondmethodSignatureForSelector
Method,
- if
There is no response
Jump toloc_6490b
, an error message is displayed - If you get
methodSignatureForSelector
theThe method signature
Nil, which is also an error
- if
methodSignatureForSelector
If the return value is not empty, theforwardInvocation
Method forinvocation
For processing
- The following is a
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 return
Message receiver
If it is still not found in the message receiver, it enters the search process of another method - If the return
nil
“, the slow message forwarding is displayed
- If the return
-
Perform to slow forward 】 【 methodSignatureForSelector method
- If the returned
The method signature
fornil
The directCollapse of the error
- If the returned method is signed
Don't is nil
And went toforwardInvocation
Method, the Invocation transaction is handled and no error is reported if it is not
- If the returned
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, foundforwardInvocation
The Method does not handle the invocation and does not crash
Can also beHandling invocation transactions
, as shown below, modifyinvocation
thetarget
for[LGStudent alloc]
, the call[anInvocation invoke]
To trigger theLGPerson
Of the classsay666
The invocation of the instance method is calledLGStudent
thesay666
methods
- (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 inforwardInvocation
In the methodWhether to handle the Invocation
Transactions, 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 thatresolveInstanceMethod
Method is executed throughlookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod
Came toresolveInstanceMethod
Source code, in the source code by sendingresolve_sel
Message firing, as shown below
-
IMP IMP = lookUpImpOrNil(inST, SEL, CLS); Add a breakpoint, print the stack through Bt and see what’s going on
-
in
resolveInstanceMethod
In the methodIMP imp = lookUpImpOrNil(inst, sel, cls);
Add a breakpoint and run the program until the first time"Come"
, using BTFirst dynamic method resolution
Is 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 throughCoreFoundation
the-[NSObject(NSObject) methodSignatureForSelector:]
Method, and then throughclass_getInstanceMethod
Going 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? through
Hopper
The disassemblyCoreFoundation
The executable file to viewmethodSignatureForSelector
Method pseudocode -
through
methodSignatureForSelector
Pseudocode entry___methodDescriptionForSelector
The implementation of the
-
Enter the
___methodDescriptionForSelector
The pseudo-code implementation, combined with the assembled stack print, can be seen in___methodDescriptionForSelector
That’s called in this methodobjc4-781
theclass_getInstanceMethod
-
Search the source code in objC
class_getInstanceMethod
, the source code implementation is shown below
This point can be passedCode debugging
To verify, as shown below, inclass_getInstanceMethod
Add a breakpoint to the method and executemethodSignatureForSelector
After the method, the signature is returned, indicating that the method signature is in effect, and apple is walking toinvocation
Before,Gives developers a chance to query again
So go toclass_getInstanceMethod
Here, go to the method query againsay666
And then I’ll go toDynamic method resolution
Therefore, the above analysis also confirms the previous articleresolveInstanceMethod
Method 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 rewrite
resolveInstanceMethod
Method, and plusclass_addMethod
Operating theAssignment IMP
At this time,resolveInstanceMethod
Will 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 of
resolveInstanceMethod
Method to assign IMP, inLGPerson
Class to rewriteforwardingTargetForSelector
Method and specify that the return value is[LGStudent alloc]
, rerun ifresolveInstanceMethod
It was printed twice, which means it wasforwardingTargetForSelector
Method executes the dynamic method resolution before, and vice versaforwardingTargetForSelector
Methods after[Conclusion] : DiscoveryresolveInstanceMethod
The print is still only printed once, ranking the second dynamic method resolution inforwardingTargetForSelector
Methods after - I’ll rewrite it in LGPerson
methodSignatureForSelector
和forwardInvocation
Run,
[Conclusion] :Second dynamic method resolution
inmethodSignatureForSelector
和forwardInvocation
Methods 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, inmethodSignatureForSelector
和 forwardInvocation
There 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 cache
To find the implementation of the specified method[Slow search process]
If it is not found in the cache, theClass method list
If still not found, go toA list of caches and methods for the parent class chain
Look for[Dynamic Method resolution]
If a slow search doesn’t find anything,First chance to fix 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 + Slow forwarding
- If not, the program will crash
unrecognized 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