Study in harmony! Not anxious not impatient!! I am your old friend Xiao Qinglong

The previous article, iOS low-level analysis, has analyzed the process of objc_msgSend from fast message lookup to slow lookup. In the slow lookup function, lookUpImpOrForward, the lookup process is:

  • Current class method list lookup
  • Parent class cache lookup
  • Superclass method list lookup
  • Continue the loop from the parent class to the parent class until the parent class is nil, giving IMP a defaultforward_imp

Part of the code for lookUpImpOrForward is as follows

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) { const IMP forward_imp = (IMP)_objc_msgForward_impcache; . for (unsigned attempts = unreasonableClassCount();;) { if (slowpath((curClass = curClass->getSuperclass()) == nil)) { ... /// When all parent classes are not found, assign the value forward_IMP IMP = forward_IMP; break; }}... }Copy the code

So let’s explore the process implementation of _objc_msgForward_impcache:

(Search for ideasIf the current file is not found, then the global search can be done. If necessary, you can remove the previous”_“Or”_ _“And then search)
STATIC_ENTRY __objc_msgForward_impcache
	b	__objc_msgForward
END_ENTRY __objc_msgForward_impcache
Copy the code

This file searches for “__objc_msgForward”

ENTRY __objc_msgForward
	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
        /** TailCallFunctionPointer = $0; /** TailCallFunctionPointer = $0; /** TailCallFunctionPointer = $0; So __objc_msgForward is going into __objc_forward_handler */
	TailCallFunctionPointer x17
	
END_ENTRY __objc_msgForward
Copy the code

Global search for “_objc_forward_handler”

__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);
}
/ / is _objc_forward_handler objc_defaultForwardHandler
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
Copy the code

If we call an unimplemented method in OC code, the console will print out this statement. If we call an unimplemented method in OC code, the console will print this statement.

  • Instance methods

  • Class method

We even see in the source code that the “+” and “-” of the error method printed by the console are added manually, so this also shows that in the underlying source code, there is no such thing as class methods, it is all object methods.

If the imp is not found, it will return the default IMP and throw an exception. If the imp is not found, it will return the default IMP and throw an exception. Next, returning to lookUpImpOrForward, we see the following code:

News feed resolution exploration

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior){...// No implementation found. Try method resolver once.
/** If the imp is not found, the imp is not found, the imp is not found, the imp is not found, the IMP is not found, the IMP is not found, the IMP is not found
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        Behavior = 1;
        returnresolveMethod_locked(inst, sel, cls, behavior); }... }Copy the code

Search for “resolveMethod_locked”

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior){.../// Call resolveInstanceMethod if it is not a metaclass
    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        Call resolveInstanceMethod if it is a metaclass
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    Behavior = 1
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
/ * * * /
Copy the code

Now let’s see what resolveInstanceMethod does,

static void resolveInstanceMethod(id inst, SEL sel, Class cls){... SEL resolve_sel = @selector(resolveInstanceMethod:); ./** send a message to CLS to determine whether to implement the resolveInstanceMethod: method, and the argument is the current sel; Class_addMethod ([self class],sel, (IMP)sayHello,"v@:@"); * /
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    /// Then look again to see if there is sel
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    / * * PrintResolving is one of these environment variable configuration, the purpose is to print resolveClassMethod: and + resolveInstanceMethod: the method of log; For now, PrintResolving returns false, so the following if can be resolved without taking */
    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)); }}/** the resolveInstanceMethod function does two things: 1, determine whether to implement resolveInstanceMethod 2, call lookUpImpOrNilTryCache to look for imp */
}
Copy the code

Now let’s look at what lookUpImpOrNilTryCache does,

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    // Select * from imp where behavior = 0
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
Copy the code
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    // Call the lookUpImpOrForward method if the CLS class has never called Initialized
    if(slowpath(! cls->isInitialized())) {// see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    // If CLS is already initialized, call cache_getImp to get imp in cache
    IMP imp = cache_getImp(cls, sel);
    
    if(imp ! = NULL) goto done;// If imp is not empty, call done
    // If imp is null, continue the if judgment
#if CONFIG_USE_PREOPT_CACHES    //CONFIG_USE_PREOPT_CACHES: whether there is cache
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    // If IMP is empty, call lookUpImpOrForward
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    // Return nil if (behavior & LOOKUP_NIL) is true and IMP is equal to _objc_msgForward_impcache
    // _objc_msgForward_impcache is forward_IMP created in lookUpImpOrForward
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
Copy the code

Why is resolveMethod_locked called twice_lookUpImpTryCache:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior){... resolveInstanceMethod(inst, sel, cls); ./ / lookUpImpOrForwardTryCache will call _lookUpImpTryCache inside
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    / / / lookUpImpOrNilTryCache will call _lookUpImpTryCache inside
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior){...done:
    /** * select * from resolveInstanceMethod where behavior=4; * by lookUpImpOrForwardTryCache behaviors = 1; (* objc_msgforward_impcache); (* objc_msgforward_impcache); Or resolveClassMethod: method, so it returns nil; * /
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
Copy the code

We found that:

  • byresolveInstanceMethodGo to _lookUpImpTryCache,behaviorThe incoming is4This is because to make a non-empty judgment, because the system gave a chance to remedy the IMP found before the call;
  • bylookUpImpOrForwardTryCacheGo to _lookUpImpTryCache,behaviorThe incoming is1So, this 1 is from the frontlookUpImpOrForwardDelta function

Singleton explain

    // Why is the if internal statement only called once
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        returnresolveMethod_locked(inst, sel, cls, behavior); } As follows0The | calculation is to add the left and right sides:2|10010 + 0001 = 0011, that is,3The &operation is to compare the values on the left and right, each bit, the same output1, different outputs0For example:3&20011 & 0010, the third bit is the same, output1, the fourth bit is different, output0As a result,0010That is2; The ^ operation is the opposite of &, different output1, same output0For example:3&20011 & 0010, the third bit is the same, output0, the fourth bit is different, output1As a result,0001That is1; Simple memory: | add & find the same ^ find the different back to the above problem, _lookUpImpOrForward global search known to be equal3, LOOKUP_RESOLVER is a constant2.3&2 = 2, soifThe judgment result istrue;
    behavior = behavior^LOOKUP_RESOLVER = 3^2 = 0011^0010 = 0001 = 1The next time you walk in,ifThe judgment becomes1&2 = 0001&0010 = 0"Does not enter into judgmentCopy the code
The dry source code analysis is finally over, and the next start of actual combat testing:

This is not implementedresolveInstanceMethod::

Next add code to DirectionChild:

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    NSLog(@"resolveInstanceMethod class:%@ - selName:%@",self,NSStringFromSelector(sel));
   
    return [super resolveInstanceMethod:sel];
}
Copy the code

We found that resolveInstanceMethod was executed twice, and we guessed that the second time was a metaclasses, but the console print found that the two addresses were the same, and this guess was denied:

MSG (CLS, resolve_sel, sel); (CLS, resolve_sel, sel); (CLS, sel);

We found that the objc_msgSend message was executed several times in the resolveInstanceMethod function, and it was sent twice to DirectionChild and its metaclasses. This explains why + (BOOL)resolveInstanceMethod (SEL) SEL is called twice.

Enter a breakpoint in the resolveInstanceMethod: method, because we were curious about why it came in the second time, so the console typed bt and printed the stack information:

We noticed that ___forwarding___ was called in the CoreFoundation framework before returning to main (see the message Forwarding article extension for more information on ___forwarding___), and as we know, ___forwarding___ internally calls the class_respondsToSelector function. We open the objc source code and search for class_respondsToSelector to see:

Continue to click_lookUpImpTryCache

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior){...if (slowpath(imp == NULL)) {
        returnlookUpImpOrForward(inst, sel, cls, behavior); }... }Copy the code

We can see inside that lookUpImpOrForward is called when the IMP is empty, so we can understand that if the IMP was not empty, it would not have entered. This means that you don’t call resolveInstanceMethod: twice. To do this, return a new message receiver at resolveInstanceMethod:.

Improve the resolveInstanceMethod code above:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod class:%@ - selName:%@",self,NSStringFromSelector(sel));
    if(sel == @selector(sayHello)) {
        Method method =class_getInstanceMethod(self,@selector(saySomething));
        /// Add the object method
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)saySomething{
    NSLog(@"What are you doing ?");
}
Copy the code

The console can print normally, and the resolveInstanceMethod is executed only once.

The same is true of class methods, which can implement the following dynamic resolution:

+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"resolveClassMethod class:%@ - selName:%@",self,NSStringFromSelector(sel));
    if(@selector(sayHowAreYou) == sel){
        Class metaClass = objc_getMetaClass("DirectionChild");
        IMP imp = class_getMethodImplementation(metaClass, @selector(saySSJ));
        Method md = class_getInstanceMethod(metaClass, @selector(saySSJ));
        const char * type = method_getTypeEncoding(md);
        
        return class_addMethod(metaClass,sel, imp,type);
    }
    return [super resolveClassMethod:sel];
}

+ (void)saySSJ{
        NSLog(@"=========saySSJ !!");
}
Copy the code

So what happens if neither resolveInstanceMethod: nor resolveClassMethod: is implemented?

For more information, see the next article, objc_msgSend Message Forwarding for iOS Underlying Analytics.

Dynamic resolution code Baidu web disk: pan.baidu.com/s/1_1B0sSeq… Password: ZSXN

Update log:

2021.07.15 17:45 -> Add resolveInstanceMethod: explanation executed twice