preface

In the process of development, there is such a phenomenon that if a method is called in the class, or the method is not implemented, the error will be reported, such as:… unrecognized selector sent to instance… . As a developer, there’s always a sense of mystery and curiosity about what you don’t know. Why does calling a method that doesn’t exist get an error like this? Next, take this point as a breakthrough, to explore, will let the children find different wonderful.

Resources to prepare

  • Objc source code: Multiple versions of objC source code
  • Ice 🍺

Enter the theme

When a class calls a method that has not been found, it reports that it has not found the method. Since this is a method lookup and the slow method lookup process described in the previous article, it is directly directed to the underlying lookUpImpOrForward() method of objC. When no corresponding method can be found in this class or any of its parent classes, a forward_IMP is returned, and the story begins there.

Method can not find the underlying principle of error reporting

Returned when the method cannot be foundimpisforward_imp

Since this source code was analyzed in detail in the last article, I will not post it all here. Only the source code required for analysis in this article is posted. According to the comments in the source code, the method slowly finds the four steps of the process: ①, ②, ③, ④ View the source code

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) { const IMP forward_imp = (IMP)_objc_msgForward_impcache; IMP imp = nil; //------------------- omit.............. for (unsigned attempts = unreasonableClassCount();;) {//----;}} {//----;} So first find it again in case if one thousand (curClass - > cache. IsConstantOptimizedCache strict (/ * * / true)) {# if CONFIG_USE_PREOPT_CACHES imp = cache_getImp(curClass, sel); if (imp) goto done_unlock; curClass = curClass->cache.preoptFallbackClass(); #endif } else { Method meth = getMethodNoSuper_nolock(curClass, sel); If (meth) {// find the method imp = meth->imp(false); goto done; // write to the cache} //---- If (slowPath ((curClass = curClass->getSuperclass()) == nil)) {imp = forward_IMP; break; } } if (slowpath(--attempts == 0)) { _objc_fatal("Memory corruption in class list."); // Superclass cache. imp = cache_getImp(curClass, sel); // Superclass cache. imp = cache_getImp(curClass, sel); if (slowpath(imp == forward_imp)) { break; } if (fastpath(imp)) { goto done; / / cache populated}} / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- code omitted... return imp; }Copy the code

Since no corresponding method is found in this class or any of its parent classes, IMP returns a forward_IMP, which is defined as _objc_msgForward_impcache according to the source code.

byforward_impThe definition of

So full text index_objc_msgForward_impcache“, and continue to look for clues (rule of thumb: generally underlined, most likely to find assembly). Real machine environment —arm64:At this point, we see through the source code, in_objc_msgForward_impcacheOnly jumps are performed inside__objc_msgForwardSo let’s look at the jump method.

In the __objc_msgForward method, the assembly instruction ADrp means to fetch the address of __objc_forward_handler in the page register x17, The LDR instruction assigns the value of the __objc_forward_Handler address to register X17, and then executes TailCallFunctionPointer x17.

Instead, we use the macro definition for TailCallFunctionPointer:

.macro TailCallFunctionPointer
	// $0 = function pointer value
	braaz	$0
.endmacro
Copy the code

If you want to jump to register X17, the address of x17 is __objc_forward_handler.

forward_impIn the end toobjc_defaultForwardHandler()

When the full-text index __objc_forward_handler is not found, then prove that the method is no longer an assembler method, but a C++ method, remove the header and search directly for objc_forward_handler. Finally found in the objc-Runtime. mm file

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

We pass objc_defaultForwardHandler as a value to * _objc_forward_Handler, and when we see the implementation of the value objc_defaultForwardHandler, it’s kind of familiar, we’re calling methods that don’t exist, Is that the format. Class_isMetaClass (object_getClass(self)) determines whether it is a class method or an instance method, prints the class name object_getClassName(self), prints the method name sel_getName(sel), and prints the class address self.

Report the underlying principle summary

  • When the method is not found in this class or any of its parent classes, IMP returns forward_IMP, and forward_IMP is defined as _objc_msgForward_impcache;

  • _objc_msgForward_impcache jumps to __objc_msgForward, and __objc_msgForward returns to the C++ method objc_forward_handler();

  • Objc_forward_handler () is passed by objc_defaultForwardHandler, which, in objc_defaultForwardHandler, is all the information that is printed.

At this point, we have a clear understanding of the underlying mechanism of the error report, we know that the current method has an error, and when the error is found, can’t we do anything else?

Now that it has been mentioned, it is obvious that there is a method of processing, and the method of processing involves an important point —- message processing flow.

Method dynamic resolution – Object method dynamic resolution

In the objc source code, create a LGPerson class and create a LGTeacher class inheriting from LGPerson, as shown in the source code below:

// Create a LGPerson class #import <Foundation/Foundation. H > @interface LGPerson: NSObject - (void)say666; @end #import "LGPerson. H "@implementation LGPerson @end // create a LGTeacher class LGPerson @end #import "LGPerson.h" @implementation LGPerson @endCopy the code

Then, inmain.mIn the initializationLGTeacherClass, and calls the parent classLGPersonthesay666Methods.When the execution reaches the breakpoint, the function is then searched to the underlying methodlookUpImpOrForward()Then hit the break point. To skipmain.mThat break point in there, it’s going to executelookUpImpOrForward()And then proceed againlldbDebug to make sure the current class is notLGTeacher:

Is to determine theLGTeacherClass, and then in the following figure for breakpoints, checkimpContent:throughlldbDebug, discoverimpIs empty. The breakpoint piece of code is aSimple interest(See:Add 1) can be executed only once.

impfornilWas given a chance to correct

After executing this simple interest, enter resolveMethod_locked() :

resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertLocked(); ASSERT(cls->isRealized()); runtimeLock.unlock(); // ---- ① ---- CLS - > isMetaClass ()) {/ / judge the current class, isn't it metaclass / / try/CLS resolveInstanceMethod: sel resolveInstanceMethod (inst, sel, CLS); } else { // try [nonMetaClass resolveClassMethod:sel] // and [cls resolveInstanceMethod:sel] resolveClassMethod(inst, sel, cls); if (! lookUpImpOrNilTryCache(inst, sel, cls)) { resolveInstanceMethod(inst, sel, cls); }} / / - (1) the return lookUpImpOrForwardTryCache (inst, sel, CLS, behaviors); }Copy the code

Because at this point, IMP is nil, and if you follow the normal process, the program crashes. The developers of Apple’s system, however, think that such a direct crash is not very friendly and not very good for the user experience. So, give iOS developers a chance to save their code (between ① comments). This means that the IMP can then be processed and returned with a non-empty IMP, ensuring that the program never crashes again.

When there is a not null imp, through lookUpImpOrForwardTryCache () method returns. Continue to track to see how the returned: enter lookUpImpOrForwardTryCache () – > _lookUpImpTryCache (), _lookUpImpTryCache () source implementation:

static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertUnlocked(); // if (slowpath(! cls->isInitialized())) { // see comment in lookUpImpOrForward return lookUpImpOrForward(inst, sel, cls, behavior); } //---- find IMP IMP = cache_getImp(CLS, sel); if (imp ! = NULL) goto done; / / - did find Shared cache # if CONFIG_USE_PREOPT_CACHES if (fastpath (CLS - > cache. IsConstantOptimizedCache strict (/ * * / true))) {imp  = cache_getImp(cls->cache.preoptFallbackClass(), sel); } #endif //---- If (slowPath (imp == NULL)) {return lookUpImpOrForward(inst, sel, CLS, behavior); } done: if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) { return nil; } return imp; }Copy the code
  • That is to say, whenimpfornilAs soon as we putimpAnd then the system will do it for us again. Although it will consume some performance.

Instance method not found, dynamic processingimp

We are currently in LGTeacher class, not metaclass, so we go inside the if judgment and enter the resolveInstanceMethod() method. To see its implementation:

static void resolveInstanceMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); //----- resolve_sel = @selector(resolveInstanceMethod:); if (! lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) { // Resolver not implemented. return; } //---- The system automatically sends a resolve_sel message to the developer, specifying the class and method names, as long as the method in the resolve_sel message is implemented, BOOL (* MSG)(Class, SEL, SEL) = (typeof(MSG))objc_msgSend; //---- Check whether the resolveInstanceMethod is successfully executed. 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. IMP IMP = lookUpImpOrNilTryCache(inst, sel, CLS); //... }Copy the code
  • In other words, when the system detects that the IMP searched is nil, the system will automatically send the resolve_sel message to the developer. As long as the method in the resolve_sel message is implemented, there will be no error again, and the implementation method is resolveInstanceMethod:.

  • When the resolveInstanceMethod: method is determined to be successful, the lookUpImpOrNilTryCache method is executed to continue the search again.

Of course, if (!) is not executed even if resolveInstanceMethod: is not implemented. Importniltrycache (CLS, resolve_sel, CLS ->ISA(/*authenticated*/true)) The system itself to the error, then will cause the system more unstable. So the system defaults to the resolveInstanceMethod method:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
Copy the code

Now that you know you need to implement the resolveInstanceMethod method, you can create a sayNB method in class LGTeacher and call the resolveInstanceMethod: method. Add an IMP dynamically

#import "LGPerson.h" @interface LGTeacher : LGPerson - (void)sayNB; @end #import "LGTeacher.h" #import <objc/message.h> @implementation LGTeacher - (void)sayNB{ NSLog(@"%@ - %s",self , __func__); } + (BOOL)resolveInstanceMethod (SEL) SEL {// handle SEL -> imp if (SEL == @selector(say666)) {imp sayNBImp = class_getMethodImplementation(self, @selector(sayNB)); Method method = class_getInstanceMethod(self, @selector(sayNB)); const char *type = method_getTypeEncoding(method); return class_addMethod(self, sel, sayNBImp, type); } NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel)); return [super resolveInstanceMethod:sel]; } @endCopy the code

callresolveInstanceMethodMethod, since there is nosay666Method, then there is a create implementation goodsayNBI’m going to replace it with. After running, no error is reported again. So we’re done.

  • Note: In the callresolveInstanceMethodMethod,NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));Will performtwoWhy this is so, you can directly go to see:Add 2The details.

Object method dynamic resolution summary

  • When an object method cannot be found, lookUpImpOrForward() returns IMP nil, then enters a simple judgment and executes resolveMethod_locked();

  • In resolveMethod_locked(), you can see that the system has given the IMP a chance to process it. If the IMP is dynamically handled and a non-empty IMP is returned, the system will search for it again to ensure that the program does not crash again.

  • The dynamic imp process is to call the resolveInstanceMethod method in the class and pass the empty IMP to the imp of an existing method, so that the imp is no longer empty and is replaced by the existing method. This completes the imp’s dynamic processing.

  • When the resolveInstanceMethod is successfully executed, bool Resolved returns to lookUpImpOrNilTryCache() and tries to find and return the method.

Method dynamic resolution – Dynamic resolution of class methods

In the resolveMethod_locked() method, when not in metaclasses, imp processing is in if judgment, when metaclasses, IMP processing should be in else judgment. Next, create another class method in the LGPerson class:

#import <Foundation/Foundation.h> @interface LGPerson : NSObject - (void)say666; + (void)sayHappy; Int main(int argc, const char * argv[]) {@autoreleasepool {[LGTeacher sayHappy]; } return 0; }Copy the code

At last come toelseIn the judgment of;

Class method not found, dynamic processingimp

Then we go inside the resolveClassMethod() method and look at its implementation, which looks very similar to the instance method dynamic resolution we just analyzed:

static void resolveClassMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); ASSERT(cls->isMetaClass()); //---- check whether the resolveClassMethod if (!) is implemented during lookUpImpOrNilTryCache lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) { // Resolver not implemented. return; } //---- perform local operations on metaclasses to prevent them from not implementing Class nonmeta; { mutex_locker_t lock(runtimeLock); nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst); // +initialize path should have realized nonmeta already if (! nonmeta->isRealized()) { _objc_fatal("nonmeta class %s (%p) unexpectedly not realized", nonmeta->nameForLogging(), nonmeta); BOOL (* MSG)(Class, SEL, SEL) = (typeof(MSG))objc_msgSend; //---- whether the resolveClassMethod is executed, but the resolveClassMethod is executed inside the class, because the class method is executed inside the class, Bool resolved = MSG (nonmeta, @selector(resolveClassMethod:), sel); // Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveClassMethod adds to self->ISA() A.K.A. CLS //---- import (int, sel, importniltrycache); //... }Copy the code
  • So the resolveClassMethod() method, that is, more local operations on the metaclass, to prevent it from not implemented, the rest, is when the system detects that the imp is nil, the system will automatically send resolve_sel message to the developer. Then you need to implement the resolveInstanceMethod: method.

  • When the resolveInstanceMethod: method is determined to be successful, the lookUpImpOrNilTryCache method is executed to continue the search again.

  • In this case, the resolveClassMethod is executed inside the class, because executing a class method inside the class is the same as executing an instance method inside the metaclass.

So next, it’s time to implement the resolveClassMethod in class LGTeacher.

#import "LGTeacher.h" #import <objc/message.h> @implementation LGTeacher + (void)sayScott{ NSLog(@"%@ - %s",self , __func__); } / / yuan as objects of a class method of + (BOOL) resolveClassMethod: (SEL) SEL {NSLog (@ "resolveClassMethod: % @ - % @", the self, NSStringFromSelector (SEL));  if (sel == @selector(sayHappy)) { IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(sayScott)); Method method = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(sayScott)); const char *type = method_getTypeEncoding(method); return class_addMethod(objc_getMetaClass("LGTeacher"), sel, sayNBImp, type); } return [super resolveClassMethod:sel]; } @endCopy the code

The biggest difference with class methods is that they are not handled fromselfFrom inside, but fromobjc_getMetaClass("LGTeacher")In the metaclass. Running result:To perform thesayScottThe method, which is the methodimpThe processing succeeds.

Performed againresolveInstanceMethodThe reason why

In the else judgment, after the resolveClassMethod is executed, we must then execute an if (!) judgment on whether inst exists. LookUpImpOrNilTryCache (inst, sel, CLS)) and finally execute the instance method resolveInstanceMethod to dynamically process the imp.

  • Because this is to determine whether or not it exists in the current classresolveInstanceMethodMethods.

According to the ISA bitmap (see Supplement 2 for details), class methods within classes are stored as instance methods within metaclasses. In this case, the imp can be dynamically processed by class method through resolveClassMethod in class, and the IMP can be dynamically processed by instance method through resolveInstanceMethod in metaclass. This is why the resolveInstanceMethod method is executed again.

Class method dynamic resolution summary

  • When a method cannot be found, lookUpImpOrForward() returns IMP nil, then enters a simple judgment and executes resolveMethod_locked();

  • In resolveMethod_locked(), perform the else judgment part to dynamically process IMP. The operation of processing is to query the class methods in the class first, execute the resolveClassMethod method, and then perform the metaclass query processing after the imp dynamic processing is completed.

  • Since executing a class method in a class is the same as executing an instance method in a metaclass, it is necessary to determine if inst exists. If it does exist, the resolveInstanceMethod method is called again and processed in the metaclass again.

  • When the resolveInstanceMethod is successfully executed, bool Resolved returns to lookUpImpOrNilTryCache() and tries to find and return the method.

withNSObject classificationincludeInstance methodsandClass methodDynamic processing ofimp

Since we’re going to use this when we’re dealing with instance methods and class methods, and NSObject is a parent class, we can do it directly with the class of NSObject:

#import "NSObject+LG.h" #import <objc/message.h> @implementation NSObject (LG) - (void)sayNB{ NSLog(@"%@ - %s",self , __func__); } + (void)sayScott{ NSLog(@"%@ - %s",self , __func__); #pragma clang diagnostic push #pragma clang diagnostic ignore "-wundeclared -selector" + (BOOL)resolveInstanceMethod:(SEL)sel{ NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel)); if (sel == @selector(say666)) { IMP sayNBImp = class_getMethodImplementation(self, @selector(sayNB)); Method method = class_getInstanceMethod(self, @selector(sayNB)); const char *type = method_getTypeEncoding(method); return class_addMethod(self, sel, sayNBImp, type); }else if (sel == @selector(sayHappy)) { IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(sayScott)); Method method = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(sayScott)); const char *type = method_getTypeEncoding(method); return class_addMethod(objc_getMetaClass("LGTeacher"), sel, sayNBImp, type); } return NO; } @endCopy the code

So we can do it all at once.

supplement

Add 1And bit operationsSimple interestThe calculation process of

Why say a simple interest?

behaviorIs made up oflookUpImpOrForward()Passed in, according to its underlying assembly,behavior = 3;Look atLOOKUP_RESOLVER, is an enumeration value,LOOKUP_RESOLVER = 2:Into theifIn judgment,behavior = behavior & LOOKUP_RESOLVER = 3 & 2 = 2And then we go into the judgment,behavior ^= LOOKUP_RESOLVERConversion,behavior = behavior ^ LOOKUP_RESOLVER = 2 ^ 2 = 0. whenbehavior = 0And then, the next time you execute that judgment,Behavior & any number, it is for0Therefore, it can no longer enter the judgment inside, that is, only execute once.behaviorThe current behavior of the tag isLOOKUP_RESOLVER.

To behaviors by value for clues into the resolveMethod_locked (), then enter lookUpImpOrForwardTryCache (), you will see

IMP lookUpImpOrNilTryCache (id inst, SEL SEL, Class CLS, int behaviors) {/ / - here you can see, the behaviors | LOOKUP_NIL is dynamic default assignment again, Means that the current is a new operation, the behaviors of the behavior is marked as LOOKUP_NIL return _lookUpImpTryCache (inst, sel, CLS, behaviors | LOOKUP_NIL); }Copy the code
  • The judgment of this bit operation is a process of marking.

Add 2, performtwoThe reason why

In the callsay666Method, cannot find the instance method, dynamic processingimp, run resolveInstanceMethodMethod was printed twice.To explore this problem, we use assembly, inresolveInstanceMethodThe first time the program is executed at the breakpoint, it is also the first time the program is printedsay666Method, as shown below:

  • By assembling BT instructions and looking at the stack information, it can be seen that the first execution process is the process we analyzed above. Slow search firstlookUpImpOrForward –> resolveMethod_locked(News Feed resolution) –>resolveInstanceMethod.

The next step is to skip the breakpoint and print a second time through assemblybtCommand to view the stack information:From the stack information, it can be seen that there is an additional message forwarding process during the second execution (article:forward). By the underlying system libraryCoreFoundationLibrary up atforwardOnce that’s done, proceed to the slow lookup process, then enter dynamic method resolution again, and invoke it againresolveInstanceMethodMethod, so the total istwo.

Or we can directly implement the methods of these processes, and see the results of the run, and also know the process of calling these methods:From the results, the order of execution remains:resolveInstanceMethod –> forwardingTargetForSelector –> methodSignatureForSelector –> resolveInstanceMethodIn the end, it was printed twice.