Welcome to the iOS Exploration series.

  • IOS explores the alloc process
  • IOS explores memory alignment &malloc source code
  • IOS explores ISA initialization & pointing analysis
  • IOS exploration class structure analysis
  • IOS explores cache_T analysis
  • IOS explores the nature of methods and the method finding process
  • IOS explores dynamic method resolution and message forwarding mechanisms
  • IOS explores the dyLD loading process briefly
  • The loading process of iOS explore classes
  • IOS explores the loading process of classification and class extension
  • IOS explores isa interview question analysis
  • IOS Explore Runtime interview question analysis
  • IOS explores KVC principles and customization
  • IOS explores KVO principles and customizations
  • IOS explores the principle of multithreading
  • IOS explores multi-threaded GCD applications
  • IOS explores multithreaded GCD underlying analysis
  • IOS explores NSOperation for multithreading
  • IOS explores multi-threaded interview question analysis
  • IOS Explore the locks in iOS
  • IOS explores the full range of blocks to read

Writing in the front

The last article talked about the method in the bottom is how to find imp through SEL, this article will be through the source code to study “no method at the bottom through how many levels to send unrecognized selector sent to instance and Crash”, After reading this article, you will understand that program crashes are also a complex process

In the dynamic method resolution source code, FXSon has two methods that only declare unimplemented methods, calling them separately:

  • - (void)doInstanceNoImplementation;
  • + (void)doClassNoImplementation;

1. Message search process

The message search process is no longer explained. The unrealized method search mainly goes through the following process:

  • Through in assemblyisaTranslation isclass, memory offset is obtainedcache->bucketsFind the cache
  • C + +
    • First look for the class cache, then look for the list of class methods
    • Traverse the parent class: look for the parent class cache, then look for the list of parent methods

Since the slow process calls lookUpImpOrForward(CLS, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/), the dynamic method is resolved after traversing the parent class without success

Second, dynamic method analysis

Dynamic method resolution is performed only when resolver and triedResolver meet the requirements

if(resolver && ! triedResolver) { runtimeLock.unlock(); _class_resolveMethod(cls, sel, inst); runtimeLock.lock();// Don't cache the result; we don't hold the lock so it may have 
    // changed already. Re-do the search from scratch instead.
    triedResolver = YES;
    goto retry;
}
Copy the code

Dynamic method resolution branches by calling method:

  • CLS isThe metaclassCall the class method, go_class_resolveClassMethod
  • A metaclassCall the instance method, go_class_resolveInstanceMethod
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if(! lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); }}}Copy the code

1. Instance method

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    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

① Check whether the SEL_resolveInstanceMethod(resolveInstanceMethod) method exists in the CLS

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}
Copy the code

Note that the resolver in lookUpImpOrForward is NO, so only the parent and parent classes are looked up and NO dynamic method resolution is performed

But CLS doesn’t have this method, and actually the root class NSObject already implements this method (NSProxy doesn’t)

// Specifically search for nsobject.mm
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

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

(SEL_resolveInstanceMethod) (SEL_resolveInstanceMethod

LookUpImpOrNil looks for imp again, fills the cache if it finds it, and returns if it doesn’t

(4) End the dynamic method resolution, go back to lookUpImpOrForward, disable triedResolver, and goto retry to find the cache and method list

2. Example method flow chart

3. The class methods

Class methods are much more complex than instance methods

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_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 = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    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

1) _class_resolveClassMethod into

    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if(! lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); }}Copy the code

LookUpImpOrNil Looks up SEL_resolveClassMethod(resolveClassMethod)

SEL_resolveClassMethod (CLS is a metaclass, _class_getNonMetaClass(CLS, inst))

LookUpImpOrNil looks for imp again, fills the cache if it finds it, and returns if it doesn’t

(5) end _class_resolveClassMethod and lookUpImpOrNil to find sel imp. If there is imp, exit dynamic method resolution. If there is no imp, enter _class_resolveInstanceMethod

⑥ Check whether the SEL_resolveInstanceMethod(resolveInstanceMethod) method exists in the CLS

⑦ Send SEL_resolveInstanceMethod message to this class

LookUpImpOrNil looks for imp again. If it finds imp, it fills the cache. If it does not find IMP, it returns imp

⑨ End dynamic method resolution, go back to lookUpImpOrForward, disable triedResolver, and goto retry

4. Flow chart of class method

5. Dynamic method resolution

Objective-c provides a tool called dynamic method resolution that allows us to dynamically provide an implementation of a selector at run time, And provide an implementation for the specified selector — subclass override +resolveInstanceMethod: or +resolveClassMethod:

  • For instance methods

As you can see from the example method flowchart, the solution to the crash is to add an alternate implementation to the resolveInstanceMethod phase

#import "FXSon.h" #import <objc/message.h> @implementation FXSon + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @ the selector (doInstanceNoImplementation)) {NSLog (@ "-- -- -- -- -- to find % @ - % @ method, collapse -- -- -- -- --", the self, NSStringFromSelector (sel)); IMP insteadIMP = class_getMethodImplementation(self, @selector(doInstead)); Method insteadMethod = class_getInstanceMethod(self, @selector(doInstead)); const char *instead = method_getTypeEncoding(insteadMethod); return class_addMethod(self, sel, insteadIMP, instead); } return NO; } - (void) doInstead {NSLog (@ "-- -- -- -- -- solve collapse -- -- -- -- --"); } @endCopy the code
  • For class methods —resolveClassMethodphase

Class methods can also plug an IMP into the metaclass (instance methods live in class objects, class methods live in metaclass objects).

#import "FXSon.h" #import <objc/message.h> @implementation FXSon + (BOOL)resolveClassMethod:(SEL)sel { if (sel == @ the selector (doClassNoImplementation)) {NSLog (@ "-- -- -- -- -- to find % @ + % @ method, collapse -- -- -- -- --", the self, NSStringFromSelector (sel)); IMP classIMP = class_getMethodImplementation(objc_getMetaClass("FXSon"), @selector(doClassNoInstead)); Method classMethod = class_getInstanceMethod(objc_getMetaClass("FXSon"), @selector(doClassNoInstead)); const char *cls = method_getTypeEncoding(classMethod); return class_addMethod(objc_getMetaClass("FXSon"), sel, classIMP, cls); } return NO; } + (void) doClassNoInstead {NSLog (@ "-- -- -- -- -- solve collapse -- -- -- -- --"); } @endCopy the code
  • For class methods —resolveInstanceMethodphase

The methods of the metaclass are stored in the root metaclass as instance methods. Since the metaclass and the root class are created by the system and cannot be modified, the corresponding instance method resolveInstanceMethod can only be dynamically resolved in the parent NSObject of the root metaclass (ISA bitmap perfectly illustrates everything).

#import "NSObject+FX.h" #import <objc/message.h> @implementation NSObject (FX) + (BOOL)resolveInstanceMethod:(SEL)sel { If ([NSStringFromSelector(sel) isEqualToString:@"doClassNoImplementation"]) {NSLog(@"—————————— can't find %@-%@ method, Collapse -- -- -- -- -- ", the self, NSStringFromSelector (sel)); IMP instanceIMP = class_getMethodImplementation(objc_getMetaClass("NSObject"), @selector(doInstanceNoInstead)); Method instanceMethod = class_getInstanceMethod(objc_getMetaClass("NSObject"), @selector(doInstanceNoInstead)); const char *instance = method_getTypeEncoding(instanceMethod); return class_addMethod(objc_getMetaClass("NSObject"), sel, instanceIMP, instance); } return NO; } - (void) doInstanceNoInstead {NSLog (@ "-- -- -- -- -- solve collapse -- -- -- -- --"); } @endCopy the code

6. Dynamic approach resolution summary

  • Instance methodsCan be rewrittenresolveInstanceMethodaddimp
  • Class methodCan be overridden in this classresolveClassMethodAdd to metaclassimpOr, inNSObject classificationrewriteresolveInstanceMethodaddimp
  • Dynamic method parsingAs long as you take any steplookUpImpOrNilFind theimpI’m not going to look it upThis classDo the dynamic method resolution, don’t go toNSObjct classificationDynamic method resolution
  • All methods are available through theNSObject classificationrewriteresolveInstanceMethodaddimpTo solve the collapse

Wouldn’t it be nice to process all crashes in NSObjct classification, prefixed to separate business logic? Wrong!

  • Unified processing of high coupling degree
  • Multiple logical judgments
  • May be inNSObjct classificationThe dynamic method resolution has been dealt with before
  • When the SDK is packaged, there needs to be a fault tolerance space

We can’t do this, we can’t do that, so what do we do? Don’t worry, Apple dad has a plan for us!

3. Message forwarding mechanism

The lookUpImpOrForward method, after looking up the class, the superclass cache and the method list, and dynamic method resolution, if it does not find the IMP, proceeds to the last step in message processing, the message forwarding process

 imp = (IMP)_objc_msgForward_impcache;
 cache_fill(cls, sel, imp, inst);
Copy the code

_objc_msgForward_impcache found in assembler, following is arm64 assembly code

Finally, we come to the _objc_forward_handler in c++

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

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);
}
Copy the code

Before the crash, the ___forwarding___ and _CF_forwarding_prep_0 methods were called, but the CoreFoundation library is not open source

At a loss what to do, lookUpImpOrForward->log_and_fill_cache->logMessageSend

From this, we can see that the log is stored in the/TMP /msgSends directory, and the objcMsgLogEnabled variable controls whether or not to store the log

bool objcMsgLogEnabled = false;
static int objcMsgLogFD = - 1;

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (- 1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = - 1;
            return true; }}// Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : The '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
Copy the code

InstrumentObjcMessageSends can change objcMsgLogEnabled values

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if(objcMsgLogFD ! =- 1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
Copy the code

So we can log and view logs according to the following code (as if not in the source code project)

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXSon *son = [[FXSon alloc] init];
        
        instrumentObjcMessageSends(true);
        [son doInstanceNoImplementation];
        instrumentObjcMessageSends(false); }}Copy the code

Shift +command+G

Dynamic method parsing
DoesNotRecognizeSelector collapse
Message Forwarding Process
Rapid process forwardingTargetForSelector
MethodSignatureForSelector slow process

1. Fast process

ForwardingTargetForSelector only a statement in the source code, and no other description, mentioned in the documentation to explain about it:

  • The return object of this method is the new object that executes SEL, that is, the object that cannot handle itself will forward the message to others for the processing of the relevant method, but cannot return self, otherwise it will never be found
  • The method is more efficient, if not implemented, will go toforwardInvocation:Method to process
  • The underlying callsobjc_msgSend(forwardingTarget, sel, ...) ;To realize the sending of messages
  • The receiver parameters, return values, etc. of the forwarded message should be the same as the original method

2. Quick process to resolve crashes

The following code resolves crashes by fast forwarding — methods that FXSon cannot implement — to FXTeacher for implementation (forward to objects that already implement the method)

# import "FXTeacher. H" @ implementation FXSon / / FXTeacher has achieved its doInstanceNoImplementation - (id)forwardingTargetForSelector:(SEL)aSelector{ NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector)); if (aSelector == @selector(doInstanceNoImplementation)) { return [FXTeacher alloc]; } return [super forwardingTargetForSelector:aSelector]; } @endCopy the code

3. Slow process

Could not find the object after forwarding the rapid process, will come to methodSignatureForSelector slow process

In accordance with the gourd ladle, found in the help document methodSignatureForSelector

Click for forwardInvocation

  • forwardInvocationandmethodSignatureForSelectorIt has to be simultaneous, and the bottom layer will generate one by signing the methodNSInvocationPass it as an argument to the call
  • Lookup can respondNSInvocation(This object does not have to be the same for all messages)
  • useanInvocationThe message is sent to the object.anInvocationThe results are saved, and the run-time system extracts the results and passes them to the original sender

4. Slow process resolution crashes

Slow process flow is methodSignatureForSelector provides a method signature first, and then forwardInvocation through the NSInvocation to implement the forwarding of a message

#import "FXTeacher.h" @implementation FXSon - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSLog(@"%s  -- %@",__func__,NSStringFromSelector(aSelector)); if (aSelector == @selector(doInstanceNoImplementation)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation{ NSLog(@"%s ",__func__); SEL aSelector = [anInvocation selector]; if ([[FXTeacher alloc] respondsToSelector:aSelector]) [anInvocation invokeWithTarget:[FXTeacher alloc]]; else [super forwardInvocation:anInvocation]; } @endCopy the code

Flow chart of message forwarding mechanism

Write in the back

Interested partners can have a look at the Demo, deepen the understanding of OC message mechanism and anti-crash application