In the previous two articles, we explored the flow of message lookups, both fast and slow, and this article begins by exploring the flow of message lookups when they are not found.

unrecognized selector

Unrecognized selector unrecognized selector is a familiar type of error in development, which is an error message when a method cannot be found, such as:

//  JSPerson.h
@interface JSPerson : NSObject

- (void)sayNB;

@end
//  JSPerson.m
@implementation JSPerson

@end
//  main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JSPerson *person = [JSPerson alloc];
        [person sayNB];
    }
    return 0;
}

Copy the code

We define a JSPerson class that declares a sayNB method, but it is not implemented, because OC is a dynamic language and method implementations can be added at run time, so no errors are reported without implementation compile time. Instantiate a JSPerson object in main and call sayNB.

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[JSPerson sayNB]: Unrecognized selector sent to instance 0x101844590’.

We started with the error at the end of the previous section. When no method implementation is found, lookUpImpOrForward returns imp = forward_IMP; _objc_msgForward_impcache namely. Objc source project search globally for this keyword and find the implementation in objC-msG-arm64.s on line 745:

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] /// Returns x17 TailCallFunctionPointer x17 END_ENTRY __objc_msgForwardCopy the code

This code is relatively simple, calling the __objc_forward_handler method, continuing to search for the __objc_forward_handler method, but not finding the method implementation, based on previous experience, Objc_forward_handler: objc_runtime.mm

// 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

So this code is pretty obvious, it’s doing an unrecognized selector error if it can’t find a method.

Dynamic method resolution

Looking at the unrecognized selector, we go back to lookUpImpOrForward, which continues after recursing through all the parent classes and fails to find the method implementation:

if (slowpath(behavior & LOOKUP_RESOLVER)) {
     behavior ^= LOOKUP_RESOLVER;
     return resolveMethod_locked(inst, sel, cls, behavior);
}
Copy the code

Behaviors is the last parameter lookUpImpOrForward method, through the find method call behaviors = LOOKUP_INITIALIZE | LOOKUP_RESOLVER = 3. LOOKUP_RESOLVER=2

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8};Copy the code

The if block is a clever use of the idea of singletons that will only be executed once:

  • Initial behavior & LOOKUP_RESOLVER =3 & 2 = 2And conditions fortrueperform
  • behavior ^= LOOKUP_RESOLVER = 3 ^ 2 = 1
  • If I do it again laterifJudgment,behavior & LOOKUP_RESOLVER = 1 & 2 = 0And conditions forfalseBlocks of code will not be executed.

The previous if block executes the method:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked(a);ASSERT(cls->isRealized());
    runtimeLock.unlock(a);// Check whether it is a metaclass
    if (! cls->isMetaClass()) {
      	// Instance method parsing
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        // Class method resolution
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls); }}// chances are that calling the resolver have populated the cache
    // so attempt using it
    // If the previous processing again to find
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
Copy the code

Instance method flow

Let’s start with the instance method, which calls the resolveInstanceMethod method:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked(a);ASSERT(cls->isRealized());
    / / resolveInstanceMethod class methods
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
      	// Because the system has a default implementation, this will not be implemented
        return;
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);Resolved indicates the return value of the method
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    // Continue the search
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            // Imp is found
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass()?'+' : The '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Resolved = true
            // 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

Add a resolveInstanceMethod: to the JSPerson class.

@implementation JSPerson

- (void)saySomethingDefalut{
    NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
        if (sel == @selector(sayNB)) {
            IMP sayDefalutImp = class_getMethodImplementation(self, @selector(saySomethingDefalut));
            Method method     = class_getInstanceMethod(self, @selector(saySomethingDefalut));
            const char *type  = method_getTypeEncoding(method);
            return class_addMethod(self, sel, sayDefalutImp, type);
        }
        return [super resolveInstanceMethod:sel];
}
@end
Copy the code

Re-run the main method and find that the program is running properly. The console prints the results

resolveInstanceMethod :JSPerson-sayNB
resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
 <JSPerson: 0x10194fab0> - -[JSPerson saySomethingDefalut]
Copy the code

By printing the result we can see that we did enter the resolveInstanceMethod and that the saySomethingDefalut method was finally executed. This is how dynamic method resolution for instance methods normally works.

Another exception is if we don’t add method handling and just print the NSLog method:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
        return [super resolveInstanceMethod:sel];
}
Copy the code

There is no doubt that the program will crash at this point, since this is equivalent to no processing, but the console will print:

resolveInstanceMethod :JSPerson-sayNB
resolveInstanceMethod :JSPerson-sayNB
-[JSPerson sayNB]: unrecognized selector sent to instance 0x10193a810
Copy the code

We added a breakpoint to the resolveInstanceMethod method and used the bt command to view the call stack:

  • The first call, the source we just explored, is the _objc_msgSend_uncached method of the LibobJC framework. The following figure shows the call information

  • The second call, the source of the message isCoreFoundationthe___forwarding___Method, which we’ll explore in the next article, calls the information shown below

Class method flow

The flow of class methods is implemented in resolveClassMethod,

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked(a);ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
      	// The input parameter is the metaclass and the return is the current class
        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;
 		// Send a message to the current 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
    IMP imp = lookUpImpOrNilTryCache(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 resolveClassMethod:%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

Add a class method (resolveClassMethod:) to the class.

@interface JSPerson : NSObject

+ (void)sayHowAreYou;

@end
  
@implementation JSPerson

+ (void)sayAndYou{
    NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"resolveClassMethod :%@-%@",self,NSStringFromSelector(sel));
        if (sel == @selector(sayHowAreYou)) {
            IMP sayAndYouImp = class_getMethodImplementation(objc_getMetaClass("JSPerson"), @selector(sayAndYou));
            Method method     = class_getInstanceMethod(objc_getMetaClass("JSPerson"), @selector(sayAndYou));
            const char *type  = method_getTypeEncoding(method);
            return class_addMethod(objc_getMetaClass("JSPerson"), sel, sayAndYouImp, type);
        }
    return [super resolveClassMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [JSPerson sayHowAreYou];
    }
    return 0;
}
Copy the code

The compile run found that the program was running properly and printed as

resolveClassMethod :JSPerson-sayHowAreYou
JSPerson - +[JSPerson sayAndYou]
Copy the code
The else {/ / try [nonMetaClass resolveClassMethod: sel] / / and/CLS resolveInstanceMethod: sel / / class method resolution resolveClassMethod(inst, sel, cls); if (! lookUpImpOrNilTryCache(inst, sel, cls)) { resolveInstanceMethod(inst, sel, cls); }}Copy the code

If we look carefully, we can now see that in the above code, which is the class method parsing process, there is a resolveInstanceMethod(inst, sel, CLS); ResolveInstanceMethod (resolveInstanceMethod, resolveInstanceMethod, resolveInstanceMethod, resolveInstanceMethod)

AOP ideas handle dynamic method resolution

With the dynamic method resolution process above, and the ISA bitmap and inheritance chain we explored earlier, we can use AOP ideas to handle dynamic method resolution of classes globally. This is done by adding a resolveInstanceMethod: class method to the NSObject class to handle the dynamic resolution of the method, for example:

@interface NSObject (JS)

@end
@implementation NSObject (LG)

- (void)saySomethingDefalut{
    NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
        if (sel == @selector(sayNB)) {
            IMP sayDefalutImp = class_getMethodImplementation(self, @selector(saySomethingDefalut));
            Method method     = class_getInstanceMethod(self, @selector(saySomethingDefalut));
            const char *type  = method_getTypeEncoding(method);
            return class_addMethod(self, sel, sayDefalutImp, type);
        }
        return NO;
}
@end
  
@interface JSPerson : NSObject

- (void)sayNB;

@end

@implementation JSPerson
  
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JSPerson *person = [JSPerson alloc];
        [person sayNB];
    }
    return 0;
}
Copy the code

The run project is found to be running properly, and the output is as follows:

resolveInstanceMethod :JSPerson-sayNB
<JSPerson: 0x100593ed0> - -[NSObject(LG) saySomethingDefalut]
Copy the code

conclusion

In this section, we mainly discuss the dynamic method resolution process, divided into instance method and class method

  • Instance methods are implemented in classesresolveInstanceMethod:Class methods that handle methods that are dynamically bound to exception handling methods.
  • Class methods are found in two parts
    1. Search for the current classresolveClassMethod:Methods.
    2. Search the metaclass of the classresolveInstanceMethod:methods
  • We can use AOP ideas in the root classclassificationIn the implementationresolveInstanceMethod:Thus complete the dynamic resolution of the global method.

Without dynamic method resolution, the message forwarding process follows, which we explore in the next section.