In the previous # OC method call objc_msgSend, we analyzed the fast lookup process of objc_msgSend, which is to find imp from cache and execute it. In this article, we will analyze the lookup process of method without cache, called slow lookup

Continuing the above, MissLabelDynamic is executed if a quick lookup is not found

. Macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant LLookupEnd\Function: LLookupRecover\Function: // Execute MissLabelDynamic method b MissLabelDynamicCopy the code

MissLabelDynamic is the third argument passed to the CacheLookup method, __objc_msgSend_uncached

. LGetIsaDone: // calls imp or objc_msgSend_uncached CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached ......Copy the code

__objc_msgSend_uncached

STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // out-of-band p15 IS the class to search MethodTableLookup TailCallFunctionPointer x17 END_ENTRY __objc_msgSend_uncachedCopy the code

MethodTableLookup

.macro MethodTableLookup SAVE_REGS MSGSEND // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) / / receiver and the selector already in x0 and x1 / / by above knowable x16 storage is a pointer to the class object mov x2, X16 mov x3, #3 If the _lookUpImpOrForward method returns a value, it is stored in the x0 register. Bl _lookUpImpOrForward // IMP in x0 // Mov x17, x0 RESTORE_REGS MSgsend.endMacro. The value of _lookUpImpOrForward is stored in x17Copy the code

TailCallFunctionPointer

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

Methodtable ELOOkUP stores IMP in X17 and passes it as a parameter to the TailCallFunctionPointer method, $0 = x17 = IMP, where the found IMP is executed.

These two methods show that the slow lookup of the method is done in the _lookUpImpOrForward method, which is the focus of our study

_lookUpImpOrForward

This is finally a return to the embrace of c++, compared to assembly language c++ is so friendly 😄

NEVER_INLINE IMP lookUpImpOrForward(id inst, SEL sel, Class cls, Int behavior) {// here returns an exception method unrecognized selector sent to... const IMP forward_imp = (IMP)_objc_msgForward_impcache; IMP imp = nil; Class curClass; runtimeLock.assertUnlocked(); If (slowpath(!) slowpath(!) cls->isInitialized())) { // The first message sent to a class is often +new or +alloc, or +self // which goes through objc_opt_* or various optimized entry points. // // However, the class isn't realized/initialized yet at this point, // and the optimized entry points fall down through objc_msgSend, // which ends up here. // // We really want to avoid caching these, as it can cause IMP caches // to be made with a single entry forever. // // Note that this check is racy as several threads might try to // message a given class for the first time at the same time, // in which case we might cache anyway. behavior |= LOOKUP_NOCACHE; } // runtimeLock is held during isRealized and isInitialized checking // to prevent races against concurrent realization. // runtimeLock is held during method search to make // method-lookup + cache-fill atomic with respect to method addition. // Otherwise, a category could be added but ignored indefinitely because // the cache was re-filled with the old value after the cache // Lock runtimelock.lock (); // We don't want people to be able to craft a binary blob that looks like // a class but really isn't one and do a CFI attack. // // To make these harder we want to make sure this is a class that was // either built into the binary or legitimately registered through // objc_duplicateClass, Objc_initializeClassPair or objc_allocateClassPair. // Check the validity of the class checkIsKnownClass(CLS); / / class and metaclass inheritance chain, not find method in the current class will look in to the parent class for CLS = realizeAndInitializeIfNeeded_locked (inst, CLS, behaviors & LOOKUP_INITIALIZE); // runtimeLock may have been dropped but is now locked again runtimeLock.assertLocked(); CurClass = CLS; curClass = CLS; // The code used to lookup the class's cache again right after // we take the lock but for the vast majority of the cases // evidence shows this is a miss most of the time, hence a time loss. // // The only codepath calling into this without having performed some // kind of cache lookup is Class_getInstanceMethod (). Class_getInstanceMethod (). Class_getInstanceMethod (). Class_getInstanceMethod (). For (unsigned attempts = unreasonableClassCount();) {// check the cache first, The execution of the cache is also changing the if (curClass - > cache. IsConstantOptimizedCache strict (/ * * / true)) {# if CONFIG_USE_PREOPT_CACHES imp = cache_getImp(curClass, sel); if (imp) goto done_unlock; curClass = curClass->cache.preoptFallbackClass(); List method meth = getMethodNoSuper_nolock(curClass, sel); #endif} list method meth = getMethodNoSuper_nolock(curClass, sel); Imp = meth->imp(false); goto done; If (slowPath ((curClass = curClass->getSuperclass()) ==) slowPath ((curClass = curClass->getSuperclass())) == nil)) { // No implementation found, And resolver didn't help. // Use forwarding. // forward_imp Error IMP = forward_IMP; break; }} // If there is a cycle in the superclass chain. _objc_fatal("Memory corruption in class list."); } // Superclass cache. // // imp = cache_getImp(curClass, sel);  If (slowpath(imp == forward_imp)) {// Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method // resolver for this class first. break; } // imp if (fastPath (imp)) {// Found the method in a superclass. Cache it in this class.goto done; }} // implementation found. Try method resolver once. Done if (slowpath(behavior & LOOKUP_RESOLVER)) {behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); } done: if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) { #if CONFIG_USE_PREOPT_CACHES while (cls->cache.isConstantOptimizedCache(/* strict */true)) { cls = cls->cache.preoptFallbackClass(); } #endif = log_and_fill_cache(CLS, imp, sel, inst, curClass); } done_unlock: runtimeLock.unlock(); if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) { return nil; } return imp; }Copy the code

_objc_msgForward_impcache

/******************************************************************** * * id _objc_msgForward(id self, SEL _cmd,...) ; * * _objc_msgForward is the externally-callable * function returned by things like method_getImplementation(). * _objc_msgForward_impcache is the function pointer actually stored in * method caches. * ********************************************************************/ STATIC_ENTRY __objc_msgForward_impcache // No stret specialization. b __objc_msgForward END_ENTRY __objc_msgForward_impcacheCopy the code

__objc_msgForward

ENTRY __objc_msgForward
    adrp x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
Copy the code

_objc_forward_handler

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

Forward_imp = (IMP)_objc_msgForward_impcache forward_IMP is an error method

checkIsKnownClass

/***********************************************************************
* checkIsKnownClass
* Checks the given class against the list of all known classes. Dies
* with a fatal error if the class is not known.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
ALWAYS_INLINE
static void
checkIsKnownClass(Class cls)
{
    if (slowpath(!isKnownClass(cls))) {
        _objc_fatal("Attempt to use unknown class %p.", cls);
    }
}
Copy the code

isKnownClass

/*********************************************************************** * isKnownClass * Return true if the class is known to the runtime (located within the * shared cache, within the data segment of a loaded image, or has been * allocated with obj_allocateClassPair). * * The result of this operation is cached on the class in a "witness" * value that is cheaply checked in the fastpath. **********************************************************************/ ALWAYS_INLINE static bool isKnownClass(Class cls) { if (fastpath(objc::dataSegmentsRanges.contains(cls->data()->witness, (uintptr_t)cls))) { return true; } auto &set = objc::allocatedClasses.get(); return set.find(cls) ! = set.end() || dataSegmentsContain(cls); }Copy the code

This determines whether the class is registered, which will be discussed later when we examine the loading process of the class

realizeAndInitializeIfNeeded_locked

/*********************************************************************** * realizeAndInitializeIfNeeded_locked * Realize  the given class if not already realized, and initialize it if * not already initialized. * inst is an instance of cls or a subclass, or nil if none is known. * cls is the class to initialize and realize. * initializer is true to initialize the class, false to skip initialization. **********************************************************************/ static Class realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize) { runtimeLock.assertLocked(); if (slowpath(! CLS - > isRealized ())) {/ / if the class has not been implemented walk this way CLS = realizeClassMaybeSwiftAndLeaveLocked (CLS, runtimeLock); // runtimeLock may have been dropped but is now locked again } if (slowpath(initialize && ! CLS ->isInitialized()) {CLS ->isInitialized()) {CLS ->isInitialized()) { CLS = initializeAndLeaveLocked(CLS, inst, runtimeLock); // runtimeLock may have been dropped but is now locked again // If sel == initialize, class_initialize will send +initialize and // then the messenger will send +initialize again after this // procedure finishes. Of course, if this is not being called // from the messenger then it won't happen. 2778172 } return cls; }Copy the code

realizeClassMaybeSwiftAndLeaveLocked

static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
    return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}
Copy the code

realizeClassMaybeSwiftMaybeRelock

/*********************************************************************** * realizeClassMaybeSwift (MaybeRelock / AndUnlock / AndLeaveLocked) * Realize a class that might be a Swift class. * Returns the real class structure for the class. * Locking: * runtimeLock must be held on entry * runtimeLock may be dropped during execution * ... AndUnlock function leaves runtimeLock unlocked on exit * ... AndLeaveLocked re-acquires runtimeLock if it was dropped * This complication avoids repeated lock transitions in some cases. **********************************************************************/ static Class realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked) { lock.assertLocked(); if (! cls->isSwiftStable_ButAllowLegacyForNow()) { // Non-Swift class. Realize it now with the lock still held. // fixme wrong In the future for objc subclasses of Swift classes // Not swift class, look at realizeClassWithoutSwift(CLS, nil); if (! leaveLocked) lock.unlock(); } else {swift class look at this, // Swift class. We need to drop locks and call the Swift. cls = realizeSwiftClass(cls); ASSERT(cls->isRealized()); // callback must have provoked realization if (leaveLocked) lock.lock(); } return cls; }Copy the code

realizeClassWithoutSwift

It’s a long method, and we’re not going to go into it, but we just need to know what it does, right

/*********************************************************************** * realizeClassWithoutSwift * Performs first-time initialization on class cls, * including allocating its read-write data. * Does not perform any Swift-side initialization. * Returns the real class structure for the class. * Locking: runtimeLock must be write-locked by the caller **********************************************************************/ Static Class realizeClassWithoutSwift(Class CLS, Class Previously) {// RW, ro, supercls, metacls...... // Update superclass and metaclass in case of remapping cls->setSuperclass(supercls); cls->initClassIsa(metacls); . }Copy the code

initializeAndLeaveLocked

// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
    return initializeAndMaybeRelock(cls, obj, lock, true);
}
Copy the code

initializeAndMaybeRelock

/*********************************************************************** * class_initialize. Send the '+initialize' message on demand to any * uninitialized class. Force initialization of superclasses first. * inst is an instance of cls, or nil. Non-nil is better for performance. * Returns the class pointer. If the class was unrealized then * it may be reallocated. * Locking: * runtimeLock must be held by the caller * This function may drop the lock. * On exit the lock is re-acquired or dropped  as requested by leaveLocked. **********************************************************************/ static Class initializeAndMaybeRelock(Class cls, id inst, mutex_t& lock, Bool leaveLocked) {// If (CLS ->isInitialized()) {if (! leaveLocked) lock.unlock(); return cls; } // Find the non-meta class for cls, If it is not already one. // The +initialize message is sent to The non-meta class object Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst); . }Copy the code

getMethodNoSuper_nolock

This method looks up in the method list of the class. All of the methods that we’ve looked at are actually not the core of today’s topic, but this is one of the core methods of today’s research

/*********************************************************************** * getMethodNoSuper_nolock * fixme * Locking: runtimeLock must be read- or write-locked by the caller **********************************************************************/ static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) { runtimeLock.assertLocked(); ASSERT(cls->isRealized()); // fixme nil cls? // fixme nil sel? Auto const methods = CLS ->data()->methods(); For (auto mlists = methods.beginlists (), end = methods.endLists(); mlists ! = end; ++mlists) { // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest // caller of search_method_list, inlining it turns // getMethodNoSuper_nolock into a frame-less function and eliminates // any store from this codepath. method_t *m = search_method_list_inline(*mlists, sel); if (m) return m; } return nil; }Copy the code

search_method_list_inline

ALWAYS_INLINE static method_t * search_method_list_inline(const method_list_t *mlist, SEL SEL) {// Do some fixup here, Int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->isExpectedSize(); If (fastpath (methodListIsFixedUp && methodListHasExpectedSize)) {/ / walk normally return here findMethodInSortedMethodList (sel, mlist); } else { // Linear search of unsorted method list if (auto *m = findMethodInUnsortedMethodList(sel, mlist)) return m; } // DEBUG does not look return nil; }Copy the code

findMethodInSortedMethodList

ALWAYS_INLINE static method_t * findMethodInSortedMethodList(SEL key, If (list->isSmallList()) {if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) { return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); }); } else { return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); }); }} else {/ / we don't study the M1, see here return findMethodInSortedMethodList (key, list, [] (method_t & m) {return m.b ig (). The name; }); }}Copy the code

findMethodInSortedMethodList

/*********************************************************************** * search_method_list_inline **********************************************************************/ template<class getNameFunc> ALWAYS_INLINE static  method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName) { ASSERT(list);  auto first = list->begin(); auto base = first; decltype(first) probe; uintptr_t keyValue = (uintptr_t)key; uint32_t count; for (count = list->count; count ! = 0; count >>= 1) { probe = base + (count >> 1); uintptr_t probeValue = (uintptr_t)getName(probe); if (keyValue == probeValue) { // `probe` is a match. // Rewind looking for the *first* occurrence of this value. // This  is required for correct category overrides. while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) { probe--; } return &*probe; } if (keyValue > probeValue) { base = probe + 1; count--; } } return nil; }Copy the code

Search methods by binary lookup

log_and_fill_cache

/*********************************************************************** * log_and_fill_cache * Log this method call. If  the logger permits it, fill the method cache. * cls is the method whose cache should be filled. * implementer is the class that owns the implementation in question. **********************************************************************/ static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) { #if SUPPORT_MESSAGE_LOGGING if (slowpath(objcMsgLogEnabled && implementer)) { bool cacheIt = logMessageSend(implementer->isMetaClass(), cls->nameForLogging(), implementer->nameForLogging(), sel); if (! cacheIt) return; CLS ->cache. Insert (sel, imp, receiver); }Copy the code

We see that the cache insert method is performed here, echoing the previous study

summary

If an instance object calls a method, then it sends a message to the object through objc_msgSend. After receiving the message, the instance object first obtains the pointer to the class object through ISA, and then obtains the pointer to the cache through memory translation. The method is searched from the buckets in the cache, which is called the fast search process. If the method list of the class object is not found, then the method list of the parent class is searched. This process is called the slow search process.

We have already looked at the process of finding the method, but it may still not be found, so there are also message dynamic resolution and message forwarding, which we will explore later.

Refer to the article

# objc_msgSend analysis – slow lookup