Write in the front: the exploration of the underlying principles of iOS is my usual development and learning in the continuous accumulation of a step forward road. I hope it will be helpful for readers to record my journey of discovery.Copy the code

The directory is as follows:

  1. Exploring the underlying principles of iOS alloc
  2. Exploration of the underlying principles of iOS structure in vivo alignment
  3. The nature of the object explored by iOS underlying principles & the underlying implementation of ISA
  4. The underlying principles of the ISA-class (part 1)
  5. The underlying principles of the ISA class (middle)
  6. The underlying principles of isA-Class (part 2)
  7. Exploring the nature of Runtime Runtime & Methods in iOS Underlying principles
  8. Objc_msgSend explores the underlying principles of iOS
  9. Runtime Runtime slow lookup process

Summary column for the above

  • Summary of the phase of iOS underlying principle exploration

preface

In previous articles, we explored the underlying process of message sending objc_msgSend in the Runtime runtime and the slow lookup process of messages. Today, we’ll focus on exploring the dynamic method resolution process of Runtime, starting with a case study. All right, let’s start today’s exploration.

start

A small case

We have a SMPerson class that defines some properties and methods that are simple:

The internal implementation is as follows, and is equally simple:

At this point in our daily development process, we will inevitably call SMPperson methods elsewhere, very simple, as follows:

The compiler will then report an error, a classic one:

SMObjcBuild[20894:318016] -[SMPerson likeGirl]: unrecognized selector sent to instance 0x1007c2a70 SMObjcBuild[20894:318016] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SMPerson likeGirl]: unrecognized selector sent to instance 0x1007c2a70' *** First throw call stack: ( 0 CoreFoundation 0x00007fff2048387b __exceptionPreprocess + 242 1 libobjc.A.dylib 0x00000001002fba80 objc_exception_throw + 48 2 CoreFoundation 0x00007fff2050638d -[NSObject(NSObject) __retain_OA] + 0 3 CoreFoundation 0x00007fff203eb90b ___forwarding___ + 1448 4 CoreFoundation 0x00007fff203eb2d8 _CF_forwarding_prep_0 + 120 5 SMObjcBuild  0x0000000100003a30 main + 64 6 libdyld.dylib 0x00007fff2032bf5d start + 1 7 ??? 0x0000000000000001 0x0 + 1 ) libc++abi: terminating with uncaught exception of type NSException *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SMPerson likeGirl]: unrecognized selector sent to instance 0x1007c2a70'Copy the code

Apparently, this is telling us that the SMPerson instance didn’t implement the method likeGirl, couldn’t find the implementation of the method, so it crashed. Why would I have reported this error if I hadn’t implemented this method? So, recall from the previous article that lookUpImpOrForward in a slow lookup process starts with a forward_IMP:

const IMP forward_imp = (IMP)_objc_msgForward_impcache;
Copy the code

At the end of the search, still did not find the IMP

if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    // No implementation found, and method resolver didn't help.
    // Use forwarding.
    imp = forward_imp;
    break;
}
Copy the code

It’s going to assign forward_IMP to the IMP and then jump out, and then back out. So, let’s see what kind of IMP this _objc_msgForward_impcache is. After a global search, we briefly searched the process, and finally found here: __objc_forward_handler, which in the OBJC2 environment is

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

// 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)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
Copy the code

So when we can’t find our method, we just print it through a format.

At this point in the program, there’s obviously been an error, so is there anything we can do about it? And apparently it can. And that’s what we’re going to explore today, dynamic method resolution.

Dynamic resolution of instance methods

The example above -[SMPerson likeGirl] method is not found. Next, we debug the process in the lookUpImpForward process with breakpoints. The process that the method cannot find will go to this branch.

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

After come to perform here, this is the case, our approach is to find, in accordance with the general logic, should is to make the program crash, however, this will is the unstable system, the user experience will be not so friendly, so apple design, here will give us a chance to, save the method could not find the problem. As long as we do this, the system will look for imp again.

/*********************************************************************** * resolveMethod_locked * Call +resolveClassMethod or +resolveInstanceMethod. * * Called with the runtimeLock held to avoid pressure in the caller * Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb **********************************************************************/ static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertLocked(); ASSERT(cls->isRealized()); runtimeLock.unlock(); if (! CLS ->isMetaClass(); CLS ->isMetaClass(); } else {// metaclass 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 return lookUpImpOrForwardTryCache(inst, sel, cls, behavior); }Copy the code

Instance method, goes to the resolveInstanceMethod branch.

/*********************************************************************** * resolveInstanceMethod * Call +resolveInstanceMethod, looking for a method to be added to class cls. * cls may be a metaclass or a non-meta class. * Does not check if the method already exists. **********************************************************************/ static void resolveInstanceMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); SEL resolve_sel = @selector(resolveInstanceMethod:); if (! lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) { return; BOOL (* MSG)(Class, SEL, SEL) = (typeof(MSG))objc_msgSend; Bool resolved = MSG (CLS, resolve_sel, sel); bool resolved = MSG (CLS, resolve_sel, sel); IMP = lookUpImpOrNilTryCache(inst, sel, CLS); if (resolved && PrintResolving) { ... }}Copy the code

So, let’s now implement the resolveInstanceMethod method in the SMPerson class and see what happens.

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

The logs are as follows:

SMObjcBuild[21877:375710] resolveInstanceMethod : SMPerson-likeGirl
SMObjcBuild[21877:375710] resolveInstanceMethod : SMPerson-likeGirl
SMObjcBuild[21877:375710] -[SMPerson likeGirl]: unrecognized selector sent to instance 0x100b98b20
Copy the code

It’s still going to crash, but it’s going to execute first what we added to the resolveInstanceMethod method. In other words, we can do processing here to avoid crashing the program. One detail to notice here is that by default NSObject implements the resolveInstanceMethod method which is the system pocket.

Dynamic resolution of class methods

In the previous section, in the metaclass branch, we found that we can handle the absence of class methods by implementing the metaclass instance method resolveClassMethod. So, we know that instance methods of a metaclass are class methods of a class. So we just implement the resolveClassMethod in our class.

extension

All instance methods that are not implemented go to the resolveInstanceMethod method and all class methods that are not implemented go to the resolveClassMethod method. So, is it too much trouble to do it for each class, we can actually create a new class of NSObject that implements two methods, so we don’t have to write it once for each class.

Why is that?

  • When Apple designed the system, when we could not find the method, when we implemented the method dynamic resolution of the two methods, it gave us the opportunity to deal with, not the program crash caused by the user experience is not friendly.
  • In addition, the global method is not implemented, so we can listen here.
  • Handle bugs in some modules globally and keep a log to notify developers of problems.
  • AOP aspect Oriented Programming (I won’t expand on that here)

instrumentObjcMessageSends()

In a casual debugging, I found a variable in the DON branchobjcMsgLogEnabledYou can control writing to the log,

After the global search, found void instrumentObjcMessageSends (BOOL flag) this method can control the value of a variable, so we will be in the file extern once, then open the log, before we crash and closed, Finally, in the/TMP /msgSends path,

Why do all methods print both sides?

What is the process after resolveInstanceMethod?

With these questions in mind, let’s break it down in our next article.

conclusion

The feature of dynamic method resolution is designed by Apple to give us the possibility of fault tolerance if the method is not found. The user unfriendly experience is avoided, so if we do not do processing during dynamic method determination, the next system processing will come to the message forwarding process. In the next post, we continue our exploration. Come on, everybody!