This article is divided into three phases: fast lookup (assembly), slow lookup (Runtime), dynamic resolution, and message forwarding

Nature of method

Before we get into the nature of the method, what is Runtime

The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.

Objective-c delays decisions as much as possible, from compilation to linking to runtime. Whenever possible, it performs operations dynamically. This means that the language requires not only a compiler, but also a runtime system to execute the compiled code. Rumtime. runtime is very common in our projects and can be used in many ways. For example:

  • [person sayHello]
  • [NSObject isKindOfClass]
  • class_getInstaceSize

The corresponding relationship between them is shown in the figure below CompilerIt’s what we call a compiler, which isLLVM. For example, a common alloc method in a project, when compiled into C or C++ becomes objC_alloc, the Runime System Library is our System’s underlying Library.

Next, we compile the source code through Clang

Person *person = [person alloc]; [person sayHello]; [person say666]; Person * Person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")); ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello")); ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("say666"));Copy the code

We see that after compiling all the methods send messages by calling objc_msgSend, so what does it do?

The following examples are based on the ARM64 architecture and objC4-781 source code

Quickly find

We searched the source code project for objc_msgSend and found no corresponding implementation in the c++ file. We saw the entry in the.s assemblyENTRY _objc_msgSend Then look at the _objc_msgSend assembly source implementation

Unwinding _objc_msgSend, NoFrame unwinding _objc_msgSend, NoFrame P0 is the first parameter to _objc_msgSend, the receiver CMP P0, #if SUPPORT_TAGGED_POINTERS // If taggedpointer is supported, B.llnilortagged // (MSB tagged pointer looks negative) #else // Fetch ISA from the object, i.e., fetch ISA from the address pointed to by the x0 register. P13 = isa LDR p13, [x0] p13 = isa GetClassFromIsa_p16 p13 = class LGetIsaDone: // calls IMP or objc_msgSend_uncached CacheLookup NORMAL, _objc_msgSend #if SUPPORT_TAGGED_POINTERS LNilOrTagged: // is equal to null, Return null b.eqlreturnZero // nil check // otherwise get isa through a series of operations and then jump LGetIsaDone // tagged ADrp x10, _objc_debug_taggedpointer_classes@PAGE add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF ubfx x11, x0, #60, #4 ldr x16, [x10, x11, LSL #3] adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF cmp x10, x16 b.ne LGetIsaDone // ext tagged adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF ubfx x11, x0, #52, #8 LDR x16, [x10, x11, LSL #3] b LGetIsaDone // SUPPORT_TAGGED_POINTERS #endif: X0 is already zero mov x1, #0 movi d0, #0 movi d1, #0 movi d2, #0 movi d3, #0 movi retCopy the code

The above is mainly to find the isa pointer corresponding to the receiver from the current message receiver, and then find the corresponding class

Next, let’s look at the GetClassFromIsa_p16 implementation of finding a class through ISA

.macro GetClassFromIsa_p16 /* src */ ..... // Here 64-bit relevant #elif __LP64__ // 64-bit Packed ISA // P16 = class = ISA & ISA_MASK(bit operation via the ISA_MASK in ISa.h Get shiftcls in ISA) and p16, $0, #ISA_MASK..... #endif .endmacroCopy the code

Finally get isa and class and start focusing on the CacheLookup process, which is CacheLookup. Let’s take a look at its source code now

.macro CacheLookup // // Restart protocol: // // As soon as we're past the LLookupStart$1 label we may have loaded // an invalid cache pointer or mask. // // When task_restartable_ranges_synchronize() is called, // (or when a signal hits us) before we're past LLookupEnd$1, // then our PC will be reset to LLookupRecover$1 which forcefully // jumps to the cache-miss codepath which have the following // requirements: // // GETIMP: // The cache-miss is just returning NULL (setting x0 to 0) // // NORMAL and LOOKUP: // - x0 contains the receiver // - x1 contains the selector // - x16 contains the isa // - other registers are set as per calling conventions // LLookupStart$1: / / p1 = SEL, p16 = isa x16 (isa) offset CACHE (CACHE = 2 * 8 or 16) to get the address of the CACHE exists in the p11 / / p11 = mask | buckets from x16 of 16 bytes (isa) in the translation, Superclass = 'isa'; superclass = 'isa'; superclass = 'superclass'; Cache contains high 16 bit mask and low 48 bit buchets) LDR p11, [x16, # CACHE] / / p11 = mask | buckets / / if it is a 64 - bit machine # you if CACHE_MASK_STORAGE = = CACHE_MASK_STORAGE_HIGH_16 / / P11 (cache)& 0x0000ffffFFFFFFFF = buckets And p10, p11, #0x0000ffffffffffff // p10 = buckets // P11 (cache) is moved 48 places to the right, so buckets is put into mask. Mask&p 1(sel, second parameter of MSG_send) is used to get hash index, which is stored in P12 (hash algorithm and cache here) Insert is the same through MASk&SEL) and p12, P1, p11, LSR #48 // x12 = _cmd & mask #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 and p10, p11, #~0xf // p10 = buckets and p11, p11, #0xf // p11 = maskShift mov p12, #0xffff lsr p11, p12, p11 // p11 = mask = 0xffff >> p11 and p12, p1, P11 // x12 = _cmd & mask #else #error Unsupported cache mask storage for ARM64. #endif //PTRSHIFT=3 P10 is the first address of buckets //((_cmd & mask) << (1+PTRSHIFT)) p12 is moved to the left by 4 bits (*16) to get the hash value. Buckets (start address)+ offset address to get bucket corresponding to index. Bucket structure {sel, Add p12, p10, p12, LSL #(1+PTRSHIFT) // buckets + ((_cmd & mask) << (1+PTRSHIFT)) LSL #(1+PTRSHIFT) // buckets + ((_cmd & mask) << (1+PTRSHIFT) P9 (store sel) LDP p17, p9, [x12] {imp, sel} = *bucket // compare sel with p1 (CMD) 1: CMP p9, p1 // if (bucket->sel! = _cmd) // If not, go to 2f b. n2f // scan more // If not, a cacheHit is hit. Imp cacheHit $0 is returned. // not hit: P12 = not-hit bucket // If the bucket cannot be found because it is normal, _objc_MSgsend_uncached CheckMiss $0 // miss if bucket->sel == 0 // p12 = buckets ) CMP p12, p10 / / wrap if bucket = = buckets / / if equal to, go to step 3 b.e q 3 f / / from x12 (p12 buckets first address) - BUCKET_SIZE actual need translation memory size, LDP p17, p9, [x12, # -bucket_size]! // {sel, sel} = * p12 = first bucket, W11 = mask #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16; if p11 (cache) moves 44 bits to the right, mask moves 4 bits to the left. To the last element of Buckets, P12 add p12, P12, p11, LSR #(48 - (1+PTRSHIFT)) // p12 = buckets + (mask << 1+PTRSHIFT) #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 add p12, p12, p11, LSL #(1+PTRSHIFT) // p12 = buckets + (mask << 1+PTRSHIFT) #else #error Unsupported cache mask storage for ARM64. #endif // Clone scanning loop to miss instead of hang when cache is corrupt. // The slow path may detect any corruption and halt later. P17, p9, [x12] LDP p17, p9, [x12] {imp, sel} = *bucket cmp p9, p1 // if (bucket->sel ! CacheHit $0 // call or return imp 2: // not hit: P12 = not-hit bucket CheckMiss CheckMiss $0 // miss if bucket->sel == 0 p12 = buckets p10 P10 // if bucket == buckets // if bucket == buckets Jump goto step 3 (namely JumpMiss $0) b.e q 3 f / / from x12 (p12 buckets first address) - actual need translation memory size BUCKET_SIZE, before get a bucket element, imp - sel respectively in p17 - p9, LDP p17, p9, [x12, # -bucket_size]! CMD b 1b // loop LLookupEnd$1: LLookupRecover$1:3: // Double wrap // jump to JumpMiss __objc_msgSend_uncached JumpMiss $0.endMacroCopy the code

Here are the CacheHit, CheckMiss, and JumpMiss assembly implementations

// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, X16 = ISa. macro CacheHit // if $0 == NORMAL TailCallCachedImp x17, x12, x1 x16 // authenticate and call imp .elseif $0 == GETIMP mov p0, p17 cbz p0, 9f // Don't ptrauth a nil IMP AuthAndResignAsIMP X0, X12, X1, X16 // Authenticate IMP and re-sign as IMP // Return IMP 9: ret // return IMP .elseif $0 == LOOKUP // No nil check for ptrauth: the caller would crash anyway when they // jump to a nil IMP. We don't care if that jump also fails ptrauth. AuthAndResignAsIMP x17, x12, x1, x16 // authenticate imp and re-sign as IMP ret // return imp via x17 .else .abort oops .endif .endmacro .macro CheckMiss // if bucket->sel == 0 // if bucket->sel == 0 LGetImpMiss. If $0 == GETIMP CBZ p9, LGetImpMiss Ached. Elseif $0 == NORMAL CBZ p9, __objc_msgSend_uncached // If LOOKUP, __objc_msgLookup_uncached. Elseif $0 == LOOKUP CBZ p9, __objc_msgLookup_uncached. Else. Abort oops.endif. endmacro is the same as checkMiss.macro jumpmiss.if $0 == GETIMP b LGetImpMiss Ached. Elseif $0 == ached B __objc_msgSend_uncached Run __objc_msgLookup_uncached. Elseif $0 == LOOKUP B __objc_msgLookup_uncached. Else. Abort oops.endif.endmacroCopy the code

Let’s go through the above implementation process

  • Get buckets by isa address offset 16 bytes

    • In the class structure, the first isa is 8 bytes, the second superclass is 8 bytes, and the third is cache, so it is offset by 16 bytes and stored in register P11
  • Fetch buckets and mask from the cache (where Mask is 16 bits higher and buckets 48 bits lower).

    • C&0x0000ffffffffffff Indicates buckets after erasing 16 bits, which is stored in register P10
    • Mask is moved 48 bits right through cache to obtain 16 bits higher, namely mask, which is stored in register P11
  • The object of msgsend, p1 (the second parameter _cmd) & MSak, is hashed to obtain the index of the bucket that needs to find sel-IMP. P12 = index = _cmd & mask. The hash value is generated in this way when the cache is stored. Therefore, the hash value must be read in the same way.

  • According to the first address of index and buckets, extract the bucket corresponding to the hash index

    • To calculate the actual size of a bucket, bucket_t contains 8 bytes of SEL and 8 bytes of IMP, so 16

    • The calculated hash index is multiplied by the memory size occupied by a single bucket (16) to obtain the offset of the index in the actual memory

    • Obtain the bucket corresponding to hash index by starting address + actual offset

  • According to the obtained bucket, take out imp and store p17, take out SEL and store P9

  • Loop recursive search

    • Compare sel in the obtained bucket with the _cmd(p1) of the second parameter of objc_msgSend

    • If they are equal, we jump directly to CacheHit, which is a CacheHit, and imp is returned

    • If they are not equal, there are two situations

      • If not, skip to CheckMiss, because $0 is normal, then skip to __objc_msgSend_uncached

      • If the bucket obtained by index is equal to the first element of buckets, set the current bucket to the last element of buckets (by moving buckets’ address +mask 44 places to the right (equal to moving mask 4 places to the left) to the last element of buckers). Then continue the recursive loop (the first recursive loop nested the second recursive loop)

      • If the current bucket is not equal to buckets’ first element, the search continues and the first recursive loop is entered

  • The second loop recurses

    • The process is basically the same as the first layer above, except that if you loop to the first bucket again, you JumpMiss directly. If ($0 = normal), we get the __objc_msgSend_uncached method, and get it cached

Here is the flow chartCached imp, cached imp, cached IMP, cached IMP, cached IMP, cached IMP, cached IMP, cached IMP

Slow to find

If we don’t find cached items, whether CheckMiss or JumpMiss, we end up with the __objc_msgSend_uncached assembly function, ached

Next, we look for the compile implementation __objc_msgSend_uncached in objC-msG-arm64.s

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p16 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
Copy the code

With the above assembly code, we continue to look at the source code for the method table

.macro MethodTableLookup // push frame SignLR stp fp, lr, [sp, #-16]! mov fp, sp // save parameter registers: x0.. x8, q0.. q7 sub sp, sp, #(10*8 + 8*16) stp q0, q1, [sp, #(0*16)] stp q2, q3, [sp, #(2*16)] stp q4, q5, [sp, #(4*16)] stp q6, q7, [sp, #(6*16)] stp x0, x1, [sp, #(8*16+0*8)] stp x2, x3, [sp, #(8*16+2*8)] stp x4, x5, [sp, #(8*16+4*8)] stp x6, x7, [sp, #(8*16+6*8)] str x8, [sp, #(8*16+8*8)] // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) // receiver and selector already in x0 and x1 mov x2, x16 mov x3, #3 bl _lookUpImpOrForward // IMP in x0 mov x17, x0 // restore registers and return ldp q0, q1, [sp, #(0*16)] ldp q2, q3, [sp, #(2*16)] ldp q4, q5, [sp, #(4*16)] ldp q6, q7, [sp, #(6*16)] ldp x0, x1, [sp, #(8*16+0*8)] ldp x2, x3, [sp, #(8*16+2*8)] ldp x4, x5, [sp, #(8*16+4*8)] ldp x6, x7, [sp, #(8*16+6*8)] ldr x8, [sp, #(8*16+8*8)] mov sp, fp ldp fp, lr, [sp], #16 AuthenticateLR .endmacroCopy the code

Look for _lookUpImpOrForward in the file and find that the assembler function is gone

Let’s debug the assembly to see the flow of the function call. First, we will make a breakpoint in the main function call method, as shown belowHere we see the underlying callobjc_msgSendDebug — Debug worlflow — check Always show Disassembly and run the program

Add a breakpoint to objc_msgSend in assembler, execute the break, hold down control + stepinto, enter objc_msgSend in assemblerAdd a breakpoint to _objc_msgSend_uncached, break it, and hold down control + stepinto to compileAs you can see, lookUpImpOrForward is the last thing that comes up, which is not an assembly implementation but a.mm C++ file. Following the instructions in the assembly section, lookUpImpOrForward is searched globally and the source code implementation is found in the objC-Runtime-new.mm file, which is a c-implemented function

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, Int behavior) {// Define message forwarding const IMP forward_IMP = (IMP)_objc_msgForward_impcache; IMP imp = nil; Class curClass; runtimeLock.assertUnlocked(); If (fastPath (behavior & LOOKUP_CACHE)) {imp = cache_getImp(CLS, sel); if (imp) goto done_nolock; } runtimelock.lock (); // Check if a class checkIsKnownClass(CLS) is already loaded; Slowpath (!); // Check whether the class is implemented. If not, implement it first. cls->isRealized())) { cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock); If (slowPath ((behavior & LOOKUP_INITIALIZE) &&! cls->isInitialized())) { cls = initializeAndLeaveLocked(cls, inst, runtimeLock); } runtimeLock.assertLocked(); curClass = cls; // unreasonableClassCount -- indicates the upper limit of class iterations for (unsigned Attempts = unreasonableClassCount();) Method meth = getMethodNoSuper_nolock(curClass, sel); Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { imp = meth->imp; goto done; If (slowPath ((curClass = curClass->superclass) == nil)) {slowpath((curClass = curClass->superclass) == nil)) { Method parsers don't work either, use forward IMP = forward_IMP; break; } // Stop if (slowpath(--attempts == 0)) {_objc_fatal("Memory corruption in class list "); } // imp = cache_getImp(curClass, sel); Slowpath (imp == forward_imp)) {// If (slowpath(imp == forward_imp)) {// If (slowpath(imp == forward_imp)) {// If (slowpath(imp == forward_imp)); } if (fastPath (imp)) {if (fastPath (imp)) {goto done; If (slowpath(behavior & LOOKUP_RESOLVER)) {if (slowpath(behavior & LOOKUP_RESOLVER)) { return resolveMethod_locked(inst, sel, cls, behavior); } done: // store to cache log_and_fill_cache(CLS, IMP, SEL, inst, curClass); / / unlock runtimeLock. Unlock (); done_nolock: if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) { return nil; } // return imp return IMP; }Copy the code

The above method mainly includes the following steps:

  • If not, return to IMP. If not, go to the next step
  • Determine the class
    • If the class is known, an error is reported if it is not
    • Class is implemented, if not, then need to implement first, determine the parent class chain, at this time the purpose of instantiation is to determine the parent class chain, RO, and RW, etc., method subsequent data reading and search cycle
    • Whether to initialize. If no, initialize
  • The loop looks in the order of the class inheritance chain or metaclass inheritance chain
    • The binary search algorithm is used to find the method in the list of methods in the current class. If it is found, it goes into the cache write process and returns IMP. If it is not found, it returns nil

    • The current class is assigned to the parent class, and if the parent class is equal to nil, IMP = the message is forwarded to forward_IMP and the recursion terminates to the next step

    • If there is a loop in the parent chain, an error is reported and the loop is terminated

    • Look up methods in the parent cache

      • If it doesn’t, it returns nil and continues the loop
      • If found, it returns to IMP and executes the cache write process
  • Determines whether dynamic method resolution has been performed
    • If not, perform dynamic method resolution

    • If dynamic method resolution has been performed once, the message forwarding process is entered

The flow chart is as follows:

Above is the whole slow search process, next we look at binary search method specific source getMethodNoSuper_nolock and process

Core code infindMethodInSortedMethodList

ALWAYS_INLINE static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list) { ASSERT(list); const method_t * const first = &list->first; const method_t *base = first; const method_t *probe; uintptr_t keyValue = (uintptr_t)key; // Key equals sayHello uint32_t count; //base = low, count = Max, probe = middle count ! = 0; Count >>= 1) {// Probe = base + (count >> 1); uintptr_t probeValue = (uintptr_t)probe->name; If (probeValue == probeValue) {// If (probeValue == probeValue) {// If (probeValue == probeValue) {// If (probeValue == probeValue) {// If (probeValue == probeValue) { While (probe > first && keyValue == (uintptr_t)probe[-1].name) {// } return (method_t *)probe; If (keyValue > probeValue) {base = probe + 1; count--; } } return nil; }Copy the code

Generally, the search process is to start from the first search, take the middle position each time, and compare it with the value of the key. If the value is equal, continue to search, and find the unequal stop. Here, we mainly consider the classification of the same name method, and then return the method of the queried position. If it is not equal, we need to continue binary search, if the loop to count = 0 is not found, then directly return nil

The flow chart is as follows:

The parent class is looked up by cache_getImp, which is assembly _cache_getImp

STATIC_ENTRY _cache_getImp

	GetClassFromIsa_p16 p0
	CacheLookup GETIMP, _cache_getImp

LGetImpMiss:
	mov	p0, #0
	ret

	END_ENTRY _cache_getImp
Copy the code

Now we see that _cache_getImp is still calling CacheLookup,
0 for G E T I M P If a method implementation is found in the superclass cache, jump to C a c h e H i t If no method implementation is found in the parent cache, jump to C h e c k M i s s or J u m p M i s s By judgment 0 is GETIMP, and a jump to CacheHit is a hit if a method implementation is found in the parent cache, or a jump to CheckMiss or JumpMiss if no method implementation is found in the parent cache
0 jumps to LGetImpMiss and returns nil, then returns IMP.

If the fast search and slow search did not find the case, it will enter the next link

Dynamic resolution and forwarding

When we do not find AN IMP in either a fast lookup or a slow lookup, we are advised to do some remedial action

  • Dynamic method resolution: neither fast lookup nor slow lookup find hu called once
  • Message forwarding: If the dynamic method resolution is not found, enter the message forwarding process

If we don’t handle either of these steps, then the system will throw an exception, so let’s test it

We add a method interface to the Person class called sayNB, which is not implemented in.m. Call sayNB from main,

@interface Person : NSObject
- (void)sayHello;
- (void)say666;
- (void)sayNB;
@end


@implementation Person
- (void)sayHello {
    NSLog(@"%s", __func__);
}

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        Person *person = [Person alloc];
        [person sayHello];
        [person say666];
        [person sayNB];
    }
    return 0;
}
Copy the code

Run the program and see the familiar crash error logWe look at the system is how to print this log, in the previous we learned that if the slow search did not find imp, will find forward_IMP, that is _objc_msgForward_impcache, we look at his source

	STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	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

The __objc_forward_handler implementation was not found in the assembly, so we go to the source code to find _objc_forward_handler and see the following code

// 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

Finally, the objc_defaultForwardHandler is called, which contains the error XXX unrecognized selector sent to instance XXX

So let’s see if the system gives me a chance to fix this, how do we fix this

Dynamic method resolution

In the slow search process did not find the method implementation, first will try a dynamic method resolution, its source code implementation is as follows:

static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertLocked(); ASSERT(cls->isRealized()); runtimeLock.unlock(); if (! CLS - > isMetaClass ()) {/ / if the CLS is a class, call the object's parsing methods / / try/CLS resolveInstanceMethod: sel resolveInstanceMethod (inst, sel, CLS); } else {// If CLS is metaclass, Call the class analysis method / / try [nonMetaClass resolveClassMethod: sel] / / and/CLS resolveInstanceMethod: sel resolveClassMethod (inst, sel, cls); // Class methods are object methods in the metaclass, so we still need to query the dynamic method resolution of the object method in the metaclass. LookUpImpOrNil (inst, sel, CLS) {// If not found or empty, look for resolveInstanceMethod(inst, sel, CLS) in the object method resolution method of the metaclass; // chances are that calling the resolver have populated the cache // so attempt using it To find out the process continued to walk return lookUpImpOrForward (inst, sel, CLS, behaviors | LOOKUP_CACHE); }Copy the code

So basically the process is

  • If CLS is a class, execute the dynamic method resolution resolveInstanceMethod of the instance method
  • If CLS is a metaclass, the dynamic method resolution resolveClassMethod executes the class method. If not found or empty in the metaclass, the dynamic method resolution resolveInstanceMethod is found in the instance method of the metaclass, mainly because the class method is an instance method in the metaclass. So you also need to look for dynamic method resolutions for instance methods in the metaclass
  • If the dynamic method resolution points its implementation to another method, the lookup continues for the specified IMP, that is, the lookUpImpOrForward process continues slowly

Let’s take a look at the example method resolveInstanceMethod implementation

static void resolveInstanceMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); SEL resolve_sel = @selector(resolveInstanceMethod:); // look is resolveInstanceMethod -- if (! lookUpImpOrNil(cls, resolve_sel, cls->ISA())) { // Resolver not implemented. return; } BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; bool resolved = msg(cls, resolve_sel, sel); // Cache the result (good or bad) so the resolver doesn't fire next time. // resolveInstanceMethod Adds to self A.K.A. CLS // find sayNB IMP IMP = lookUpImpOrNil(INST, SEL, CLS); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', 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() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); }}}Copy the code

The above process is roughly like this

  • Before sending the resolveInstanceMethod message, we need to check whether the CLS class has the implementation of the resolveInstanceMethod method. That is, lookUpImpOrNil is used and lookUpImpOrForward is used to find the resolveInstanceMethod method
  • If not, return directly
  • If so, a resolveInstanceMethod message is sent

Let’s rewrite the Person resolveInstanceMethod method

+(BOOL)resolveInstanceMethod (SEL) SEL {if (SEL == @selector(sayNB)) {NSLog(@"%@ ", NSStringFromSelector(SEL)); IMP imp = class_getMethodImplementation(self, @selector(sayError)); SayMethod = class_getInstanceMethod(self, @selector(sayError)); Const char *type = method_getTypeEncoding(sayMethod); SayError return class_addMethod(self, sel, imp, type); } return [super resolveInstanceMethod:sel]; }Copy the code

SayError (sel) = sayNB (sayError) = sayNB (SEL) = sayNB (sayError) = sayNB (sayErrorWhat if we could rewrite resolveInstanceMethod without sel and IMP? /Print the result, you will find that after two walks, sayNB comes, why, this later more details/

Add + (void)sayClass and override + (BOOL)resolveClassMethod (SEL) SEL

+ (BOOL)resolveClassMethod:(SEL) SEL {if (SEL == @selector(sayClass)) {NSLog(@"resolveClassMethod--%@ ", NSStringFromSelector(sel)); IMP imp = class_getMethodImplementation(self, @selector(sayError)); Method sayMethod = class_getInstanceMethod(self, @selector(sayError)); const char *type = method_getTypeEncoding(sayMethod); return class_addMethod(objc_getMetaClass("Person"), sel, imp, type); } return [super resolveClassMethod:sel]; }Copy the code

Print the resultNote here that the CLS passed in is no longer a class, but a metaclass. You can get the metaclass of the class through the objc_getMetaClass method, where the class methods are instance methods.

forward

In the process of slow search, we learned that if fast + slow did not find a way to achieve, dynamic method resolution is not good, then use message forwarding, but the source code also did not find the relevant source code of message forwarding, then we can through the following two ways to understand the program crash before all the way to what method

InstrumentObjcMessageSends way print sends the message log

Through lookUpImpOrForward – > log_and_fill_cache – > logMessageSend, found at the bottom of the logMessageSend source instrumentObjcMessageSends source code to achieve, so, In the main call instrumentObjcMessageSends print method call log information

The preparatory work

1, open the objcMsgLogEnabled switch, namely call instrumentObjcMessageSends method, introduced to YES

2, in the main by extern declarations instrumentObjcMessageSends method, to run the program, and to the/TMP/msgSends directory, found that msgSends beginning of log files, open found before the crash, perform the following methodResolveInstanceMethod method carried out twice, forwardingTargetForSelector method performs twice. MethodSignatureForSelector + resolveInstanceMethod were performed twice

Hopper /IDA decompilation (hopper for example)

We see it in the crash logs

___forwarding___fromCoreFoundationWe passedimage listRead the image file, and then search for CoreFoundation to see the path to its executableFollow the file path to the CoreFoundation executableThe following isforwarding_prep_0_ pseudo code

Fast forward

Here you see that _ is calledforwardingBelow is the ____forwarding___ pseudocodeIf there is no implementation forwardingTargetForSelector jump to loc_64c47, else can response but also jump to loc_64c47 the return value is empty

Slowly forward

If there is no implementation to methodSignatureForSelector loc_64fb7, perform CFLog, finally jump loc_6501cIf it does, return null jump to loc_6501cEventually perform doesNotRecognizeSelector method, if methodSignatureForSelector could enforce _forwardStackInvocation response and not nullIf null is returned jump to loc_64DF9The forwardInvocation method handles the invocation

So far, the whole process of message forwarding is as follows:The processing of message forwarding is divided into two parts:

  • “Fast forward” when slow lookup, resolution and dynamic methods were not find implementation, for message forwarding, the first is a quick message forwarding, namely forwardingTargetForSelector method

If the message receiver is returned and still not found in the message receiver, the look-up process for another method is entered

If nil is returned, slow message forwarding is entered

  • Perform to slow forward 】 【 methodSignatureForSelector method

If the method returned is signed nil, it crashes

If the returned method is not signed nil, go to the forwardInvocation method and the invocation transaction is handled without an error.

Analysis of resolveInstanceMethod is performed twice

ResolveInstanceMethod () {Method (); Description is called after the resolveInstanceMethod dynamic resolution.In resolveInstanceMethod a breakpoint, through resolveInstanceMethod stack information, can be found is methodSignatureForSelector method of tuning up

Through the disassembly in CoreFoundation [NSObject methodSignatureForSelector:] can see the whole implementation.Call the class_getInstanceMethod methodSignatureForSelectorWe debug the print stack with breakpoints and see that this is exactly where it isThrough the above argument, we know that in fact in slow forward process, between methodSignatureForSelector and forwardInvocation method and a dynamic method for resolution, the apple to give a chance again, as shown in the figure belowAll here the whole message search, forwarding process will go through the main experienceFast search -- > slow search -- > dynamic resolution -- > fast forward -- > Slow forwardThese stages