preface

The previous part of objc_msgSend Quick Lookup analyzes the fast lookup process. If the fast lookup fails, you need to go to the __objc_msgSend_uncached slow lookup process.

The following analysis of the specific process of slow search.

__objc_msgSend_uncached compilation analysis

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// Call MethodTableLookup (which involves slow lookup logic)
MethodTableLookup
/ / returns the imp
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
Copy the code
  • MethodTableLookup calls the slow lookup process.

  • TailCallFunctionPointer is to distinguish models and jump out.

MethodTableLookup assembles analysis

.macro MethodTableLookup
    // Protect the site
    SAVE_REGS MSGSEND

    // x0 = receiver
    // x1 = sel
    // x2 = cls
    mov	x2, x16
    // x3 = 3
    mov	x3, #3
    // call lookUpImpOrForward in the c++ layer
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // lookUpImpOrForward(x0, x1, x2, x3)
    bl	_lookUpImpOrForward

    // IMP in x0 returns x17
    mov	x17, x0

    // Restore the scene
    RESTORE_REGS MSGSEND
.endmacro
Copy the code
  • SAVE_REGS: Protect the site; Restore_regs: The site is restored. Often used when calling C++ functions.

  • MethodTableLookup records Receiver, SEL, CLS, and calls _lookUpImpOrForward(which is not an assembly method).

  • When a C/C++ method is called in assembly, it is necessary to remove an underscore from the method called in assembly, namely C/C++ method.

LookUpImpOrForward (slow lookup)

According to the features of the assembly lookup C++ function, remove an underline lookUpImpOrForward and perform a global search.

The goal of the lookUpImpOrForward process is to find the IMP corresponding to SEL:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // behavior = 3 (LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // Define the message forwarding IMP
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked(a);// Determine whether the class is initialized. Inside is the logical shift value of the bits.data().flags of the metaclasses of the class.
    if (slowpath(! cls->isInitialized())) {
        // If the behavior is not initialized, add LOOKUP_NOCACHE configuration.
        // The first message sent to a class is usually +new or +alloc or +self, which is new.
        // If you do not set LOOKUP_NOCACHE, the IMP cache will always leave only one entry.
        behavior |= LOOKUP_NOCACHE;
    }

    // Keep runtimeLock to prevent conflicts with multithreaded access.
    runtimeLock.lock(a);// Check whether the class has been loaded and whether the class was loaded by dyld
    checkIsKnownClass(cls);    
    // Initialize classes and metaclasses
    // The purpose is to associate classes, determine the parent class chain, method subsequent loop
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked(a);// The current class is assigned
    curClass = cls;
    
    UnreasonableClassCount: indicates the upper limit of class iteration
    // Loop until you get a return or break
    for (unsigned attempts = unreasonableClassCount();;) {
        // Look up the shared cache for one of the cache lookups when this method has been written.
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
// __arm64__ && IOS && ! SIMULATOR && ! MACCATALYST
#if CONFIG_USE_PREOPT_CACHES
            // Call the _cache_getImp assembly method to query the IMP against sel from the shared cache
            imp = cache_getImp(curClass, sel);
            // If the IMP is found, jump to the done_UNLOCK process
            if (imp) goto done_unlock;
            // Get the front class
            curClass = curClass->cache.preoptFallbackClass(a);#endif
        } else {
            // Find methodList from the class
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // If you can find the method, get imp and jump to done function
                imp = meth->imp(false);
                goto done;
            }
            // During the loop, look for getSuperclass() for the current class. If the parent class is nil, the current class is NSObject
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // If neither is found, use forward_IMP
                imp = forward_imp;
                break; }}// Stop if a loop exists in the parent class
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
        // Get the superclass cache (_cache_getImp assembly method)
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // If forward_IMP is found in the parent class, the search stops, but does not cache, and first calls the method parser for that class.
            break;
        }
        if (fastpath(imp)) {
            // Find imp in the parent class. Cache in this class.
            gotodone; }}// If imp is not found, perform a method resolution
    // The reason for executing once:
    If, behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 1. If, behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 1
    Behavior = 3, LOOKUP_RESOLVER = 2, 1 & 2 = 0
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        // Dynamic method resolution
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    // behavior = 3, LOOKUP_NOCACHE = 8, 3 & 8 = 0
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        // Look up the current class from the shared cache
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass(a); }#endif
        // Insert sel and IMP into the cache
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    / / unlock
    runtimeLock.unlock(a);If (behavior & LOOKUP_NIL) is true and imp == forward_IMP is not found, return nil
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
Copy the code
  • Get forward_IMP from the assembly function _objc_msgForward_impcache

  • Check whether the class initializes isInitialized(). Objective: Whether to add LOOKUP_NOCACHE configuration to behavior. The first message sent to the class is usually +new or +alloc or +self, which is new to the memory. If you do not set LOOKUP_NOCACHE, the IMP cache will always leave only one entry.

  • Check whether a class is loaded by dyLD: checkIsKnownClass → isKnownClass → allocatedClasses. Loaded classes are stored in allocatedClasses. We’ll talk more about allocatedClasses later.

  • Initialize the class and the metaclass realizeAndInitializeIfNeeded_locked, the purpose is for the sake of association class, determine the parent chain, facilitate subsequent cycles.

  • Enter for search IMP, infinite loop search, only break, return, goto to stop the loop, otherwise always search.

    • Find the shared cache: call _cache_getImp to quickly look up sel and query imp from the shared cache based on SEL. If it can be found, jump done_unlock.

    • Look for methodList in the current class and jump to done if you can find imp.

    • If the imp is found, determine whether the imp is equal to forward_IMP. If the IMP is equal to FORward_IMP (not found), run a dynamic method resolution resolveMethod_locked. If not (found), skip done.

    • If the parent class is not found, look for the parent class of the parent class until it finds NSObject, at which point IMP = forward_IMP (not found), a dynamic method resolution resolveMethod_locked will also be executed.

  • Done:

    • Look up the current class from the shared cache

    • Log_and_fill_cache: Inserts the queried SEL and IMP into the cache.

  • Done_unlock: Determine whether (Behavior & LOOKUP_NIL) && IMP == forward_IMP) is true. If so, return nil, otherwise return IMP.

_objc_msgForward_impcache compilation analysis

STATIC_ENTRY __objc_msgForward_impcache

// Read the base address of __objc_forward_handler into register X17
adrp x17, __objc_forward_handler@PAGE
// Read Forward IMP (x17 = x17 + __objc_forward_handler base offset)
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
// Return Forward IMP
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward_impcache
Copy the code

Get forward_IMP from the address offset according to __objc_forward_handler

IsInitialized () analysis

bool isInitialized(a) {
    // Flags status of the metaclass class_rw_t
    // RW_INITIALIZED = (1<<29)
    return getMeta() - >data()->flags & RW_INITIALIZED;
}
Copy the code

To determine whether a class is initialized, check the flags state in class_rw_T and the value of RW_INITIALIZED in its metaclasses.

realizeAndInitializeIfNeeded_locked

Class initialization, followed by a detailed study. The goal here is to get the class information ready, primarily the method list ready.

resolveMethod_locked

Dynamic method resolution, followed by detailed exploration

Log_and_fill_cache () analysis

log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        // Print the log of sending messages
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if(! cacheIt)return;
    }
#endif
    // Insert cache
    cls->cache.insert(sel, imp, receiver);
}
Copy the code
  • Log print

  • Cache. insert Cache insert

getMethodNoSuper_nolock

getMethodNoSuper_nolock(Class cls, SEL sel) {
    // Get a methodList for the class
    auto const methods = cls->data() - >methods(a);// Loop lookup. The element object is method_list_t, possibly a two-dimensional array. (Dynamic loading of methods and classes)
    for (auto mlists = methods.beginLists(), end = methods.endLists(a); mlists ! = end; ++mlists) {// Find the inline method for method_list_t (method_list_t might be a two-dimensional array)
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
Copy the code
  • Methods () : stored in the class_rw_t structure of the class.

  • Loop lookup, maybe a two-dimensional array. (Dynamic loading method or dynamic loading class)

  • The search_method_LIST_inline method obtains the IMP in two ways, sequential and non-sequential.

search_method_list_inline

static method_t *search_method_list_inline(const method_list_t *mlist, SEL sel) {
    // Check whether the order of the method list has been corrected
    int methodListIsFixedUp = mlist->isFixedUp(a);// Get the expected size of the method
    int methodListHasExpectedSize = mlist->isExpectedSize(a);if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        // If the order of the method list has been corrected (binary search method)
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Otherwise find the list of unsorted methods linearly
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }
    return nil;
}
Copy the code
  • Determine whether the method list is sorted

  • The linear search: findMethodInUnsortedMethodList – findMethodInUnsortedMethodList – for loop for imp.

  • The binary search: findMethodInSortedMethodList – findMethodInSortedMethodList – binary search.

Binary Search method (Emphasis)

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName) {
    // Binary search method
    auto first = list->begin(a);// The first method is at position 0
    auto base = first; // Start at 0
    decltype(first) probe; // Each pointer traversed
    uintptr_t keyValue = (uintptr_t)key; // The pointer passed in
    uint32_t count; // Let's say it's 8
    for(count = list->count; count ! =0; count >>= 1) {
        Prode = cardinality + total >> 1
        probe = base + (count >> 1);
        // Get the name value of prode
        uintptr_t probeValue = (uintptr_t)getName(probe);
        // If it is equal to the value passed in
        if (keyValue == probeValue) {
            // Search for the same category sel. If there's a match, go to the category. Because the categories are at the front, you go all the way to the beginning.
            // The method is stored in the class method first, then the classification, according to the principle of "first in, then out".
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                // If there are two categories, it depends on who loads first
                probe--;
            }
            // Return the address of the pointer
            return &*probe;
        }
        // If the target value > the traversal value
        if (keyValue > probeValue) {
            // Cardinality = current prode + 1
            base = probe + 1;
            / / - totalcount--; }}return nil;
}
Copy the code
  • First: The start of the list of methods, used for comparison.

  • Base: a base value. To minimize comparison, base is the element next to the one currently being looked for when the search continues.

  • Probe: a temporary variable in each loop. The user obtains sel and compares it with the key.

  • Count >>= 1: equivalent to count / 2

Code to simulate

int findSortedMethodList(int key, int listCount) {
    int first = 0; // Start position
    int base = 0; // Base position
    int probe = 0; // Each number traversed
    int times = 0;
    printf("Target key = %d\n", key);
    for (intcount = listCount; count ! =0; count >>= 1) {
        times++;
        // The value traversed
        probe = base + (count >> 1);
        printf("Count = %d, base = %d, probe = %d\n", times, count, base, probe);
        // If it is equal to the value passed in
        if (key == probe) {
            / / find
            printf("Found it! probe = %d\n", probe);
            return probe;
        }
        // If the target value > the traversal value
        if (key > probe) {
            // Cardinality = current prode + 1
            base = probe + 1;
            / / - totalcount--; }}printf("Can't find \n");
    return - 1;
}
Copy the code

Find the flow chart slowly

case

  • Defining a classZLObjectAnd its subclassesZLSubObject
@interface ZLObject : NSObject

+ (void)classMethod;

- (void)instanceMethod1;

- (void)instanceMethod2;

@end

@implementation ZLObject

+ (void)classMethod {
    NSLog(@"%s",__func__);
}

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

@end
Copy the code
@interface ZLSubObject : ZLObject

- (void)subInstanceMethod;

@end

@implementation ZLSubObject

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

@end
Copy the code
  • Adding a call method
ZLSubObject *subObj = [[ZLSubObject alloc] init];
[subObj subInstanceMethod];
[subObj instanceMethod1];
[subObj instanceMethod2];
Copy the code
  • Print the result
-[ZLSubObject subInstanceMethod]
-[ZLObject instanceMethod1]
-[ZLSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006440e0
Copy the code

SubInstanceMethod and instanceMethod1 implement methods and print normally

InstanceMethod2 has no implementation method and therefore crashes.

  • The analysis stack is as follows:

Crashes in doesNotRecognizeSelector method and is an object method.

  • Search in the source codedoesNotRecognizeSelector:

  • Add aNSObjectThe classification of
@interface NSObject (AddMethod)

- (void)categoryInstanceMethod;

@end

@implementation NSObject (AddMethod)

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

@end
Copy the code
  • Adding a call method
[ZLSubObject performSelector:@selector(categoryInstanceMethod)]
Copy the code
  • Print the result
-[NSObject(AddMethod) categoryInstanceMethod]
Copy the code

Executed successfully and is still an object method.

If the current class does not find the IMP, it will look for the methods of its parent class. The NSObject metaclass’s parent class is still NSObject, so it can find the corresponding proxy method.

Doubt point

1. Why does objc_msgSend use assembly? Instead of C/C++?

  • Assembly is closer to machine language and faster.
  • High security.
  • More dynamic. For example, when C language calls a method, the parameter must pass values, but some assembly parameters do not need to pass values, if the parameter list is the last.

2. Sorting is required before binary search, when sorting is completed?

From the method_t structure, search for keywords related to sort. The following methods were found:

struct SortBySELAddress :
public std::binary_function<const struct method_t::big&,
                            const struct method_t::big&, bool> {
    bool operator(a) (const struct method_t::big& lhs,
                     const struct method_t::big& rhs)
    { returnlhs.name < rhs.name; }};Copy the code

The following call stack is found after the break point:

Conclusion: The sorting by sel address is registered by calling this method when the _read_images class loads the map.