1. The nature of OC method calls

Put a simple method call code in the main method and compile it using the clang command.

 ZPerson *person = [ZPerson alloc];
 [person sayHello];
Copy the code

It is then compiled using the clang-rewrite-objc main.m command. The following compiled source code is obtained:

 ZPerson *person = ((ZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
Copy the code

After casting is removed:

objc_msgSend(objc_getClass("ZPerson"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));
Copy the code

From the above code, we can see that both alloc and sayHello calls are implemented through the objc_msgSend function. So we can say that the essence of the OC method is to send messages through objc_msgSend. Objc_msgSend contains two hidden arguments for the method call: self (message receiver) and sel (method number). Sel_registerName is the equivalent of @selector() in oc, which gives an SEL based on the method name passed in.

2.objc_msgSend

2.1 objc_msgSend method implementation

Find the objc_msgSend method in the source code. You can only find the declaration of the method definition. You can’t click on it to see the implementation of the method.

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0); OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);Copy the code

This is because objc_msgSend is implemented in assembly language. There are two reasons:

  • In terms of performance, method calls need to be processed and responded to quickly, while assembly is easier for machines to recognize.
  • C and C ++, as static languages, do not meet this requirement because of unknown parameters (unknown number, unknown type, such as NSLog()).

As a rule of thumb, we underline objc_msgSend and continue our search for _objc_msgSend to find the source assembler for objc_msgSend. Obcj provides different versions of the method implementation according to different platforms. We chose the ARM64 version for analysis. ENTRY is the ENTRY mark of the method, and we will analyze it from the beginning.

2.2 _objc_msgSend

Find ENTRY _objc_msgSend in objC-msG-arm64.s, which is the ENTRY to the _objc_msgSend function. My assembly level is limited, according to the information found on the Internet, source code made the following notes.

//_objc_msgSend method ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame // p0 compares with 0 to determine whether the receiver exists. // p0 is the first parameter of objc_msgSend, Message receiver CMP P0, #if SUPPORT_TAGGED_POINTERS // // (MSB tagged Pointer looks.) // Jump to LNilOrTagged and execute the Taggend Pointer function search Negative) #else // p0 = 0, // LReturnZero sets 0 to return nil and terminates _objc_msgSend b.eclreturnZero #endif // but if the method receiver is not nil, it continues // p0-p7 is the register for receiving function arguments // Fetch isa from the address that the x0 register points to. LDR p13, [x0] // p13 = isa X0 // p16 = class LGetIsaDone: // calls IMP or objc_msgSend_uncached // Calls IMP or objc_msgSend_uncached // Calls IMP or objc_msgSend_uncached  CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached #if SUPPORT_TAGGED_POINTERS LNilOrTagged: // nil check, If it is nil, go to LReturnZero. If it is nil, go to LReturnZero. GetTaggedClass b LGetIsaDone // SUPPORT_TAGGED_POINTERS #endif LReturnZero: Movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0, movi d0 #0 // return end ret // LExit end _objc_msgSend END_ENTRY _objc_msgSendCopy the code

The _objc_msgSend method flow should be familiar with the structure of objects and classes. The first is to check if the method receiver is nil. We then get the ISA from the object, and then the class according to the ISA. We know that methods of objects exist in class. Once you have the class, you can go to the method signature (SEL) to find the method implementation (IMP). There are two ways to find AN IMP in a class:

  • One is to usecache_tTo see if there are any in the cacheQuickly find
  • Secondly, the use ofclass_data_bits_tStep by step, you can refer to the previous class structure of the data store. Not as efficient as the first method, also known asSlow to find

objc_msgSendThe rough flow is these, can unite the train of thought as follows. The search flow for the implementation of specific methods is shown belowCacheLookupIn the analysis.

3.CacheLookup Quick lookup

CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant
Copy the code

NORMAL | GETIMP | LOOKUP represent three different modes:

  • NORMAL, normal mode, find the correspondingIMPAnd perform
  • GETIMPTo findIMPAnd return
  • LOOKUP, only find IMP


represents the function in which to use the CacheLookup code block. For example, the _objc_msgSend function, when used, is passed _objc_msgSend.

CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
Copy the code

MissLabelDynamic, MissLabelConstant: Code block executed when no cache can be found, passed in when CacheLookup is executed externally. MissLabelConstant is only used in GETIMP mode.

When CacheLookup is ready to execute, p1 = sel, p13 = ISA, p16 = class

When the execution ends, if the corresponding IMP is found in the cache,x16 = class, x17 = IMP. If not, x15 = class

3.1 CacheHit

CacheHit is the code macro definition specified when a CacheHit occurs. Cache hit: X17 IMP address, X10 buckets address, X1 store SEL, X16 store class pointer

.macro cachehit. if $0 == NORMAL // NORMAL means that normally the function execution is found in the cache and returns // TailCallCachedImp defined in arm64-ASM.h // validate and execute IMP TailCallCachedImp x17, x10, x1, X16 // authenticate and call imp. Elseif $0 == GETIMP // GETIMP only finds IMP in cache // p17 is cached IMP, // If p0 is 0, go to label 9. Ret CBZ P0, 9f don't ptrauth a nil IMP AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP // return IMP 9: Ret // return IMP. Elseif $0 == LOOKUP // LOOKUP for 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, X10, X1, X16 authenticate IMP and re-sign as IMP CMP X16 x15 cinc x16, x16, ne // x16 += 1 when x15 ! = x16 (for instrumentation ; Fallback to the parent class) ret // return IMP via x17. else //. Abort // Show us the current register status, stack contents, and the full Call trace to help us locate the error. .abort oops.endif. Endmacro // Ends the CacheHit assembly macro definitionCopy the code

3.2 TailCallCachedImp

When a cache hit occurs, TailCallCachedImp is executed if the cache’s lookup mode is NORMAL. Verify and execute IMP.

.macro TailCallCachedImp // eOR exclusive or Eor {condition}{S} Rd, Rn, operand eor{condition}{S} Rd, Rn, operand eor{condition}{S} Rd, Rn, operand $0 = cached IMP, $1 = address of cached IMP, $2 = SEL, $3 = isa // And place the result in $1 (mix SEL into ptrauth modifier) eor $1, $1, $2 // Mix SEL into ptrauth modifier eor $1, $1, $2 // Mix SEL into ptrauth modifier $3 // Mix ISA into ptrauth Modifier and execute braB $0, $1.endMacro in the ptrauth modifier branchCopy the code

3.3 __objc_msgSend_uncached

__objc_msgSend_uncached is used to look for methods cached in _objc_msgSend. If the corresponding IMP is not found in the cache, __objc_msgSend_uncached is executed to enter the slow lookup process, which will be delved into later.

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_uncached
Copy the code

3.4 CacheLookup

Let’s start with the main topic of this section — CacheLookup.

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
    // 重启协议:
	//
	//   As soon as we're past the LLookupStart\Function label we may have
	//   loaded an invalid cache pointer or mask.
    //   一旦超过 LLookupStart$1 标签,我们可能已经加载了无效的 缓存指针 或 掩码。
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd\Function,
	//   then our PC will be reset to LLookupRecover\Function which forcefully
	//   jumps to the cache-miss codepath which have the following
    //   requirements:
    //   当我们在超过 LLookupEnd$1 之前(或当 信号   命中我们)调用task_restartable_ranges_synchronize(),我们的 PC 将重置为  LLookupRecover$1,这将强制跳转到缓存未命中的代码路径,其中包含以下内容。

	
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
    //     缓存未命中只是返回 NULL
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver // x0 存放函数接收者 (就是我们日常的 self)
	//   - x1 contains the selector // x1 存放 SEL (就是我们日常的 @selector(xxxx))
	//   - x16 contains the isa     // x16 是 class 的 isa (也就是 self 的 isa,根据它来找到对象所属的类)
	//   - other registers are set as per calling conventions // 其它寄存器根据调用约定来设置
	//

//将原始的isa存储到x15
	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS

//#define CACHE            (2 * __SIZEOF_POINTER__)   就是16
//将存储器地址为(x16+16)的字数据读入寄存器 p10
//类的地址偏移16个字节后,刚好是cache_t的起始地址,地址上的数据, 也就是cache_t的第一个成员变量_bucketsAndMaybeMask
//将cache的内容读取到p10
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
//p10存储的数据右移48位 后 存入p11, 此时11为mask
	lsr	p11, p10, #48			// p11 = mask
//p10 和 bucketsmask 与运算后 再存入p10  此时p10存的是buckets地址
	and	p10, p10, #0xffffffffffff	// p10 = buckets
//w小端模式 x1是sel x11是mask
//计算后 x12存的的当前sel经过hash计算后得到的key
	and	w12, w1, w11			// x12 = _cmd & mask   哈希计算下标

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	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

// 在 Project Headers/arm64-asm.h 中可以看到 PTRSHIFT 的宏定义
/*
    #if __arm64__
    #if __LP64__ // 64 位系统架构
    #define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE // 0b1000 表示一个指针 8 个字节
    // "p" registers are pointer-sized
    // true arm64
    #else
    // arm64_32 // 32 位系统架构
    #define PTRSHIFT 2  // 1<<PTRSHIFT == PTRSIZE // 0b100 表示一个指针 4 个字节
    // "p" registers are pointer-sized
    // arm64_32
    #endif
 */


// p12, LSL #(1+PTRSHIFT) 将p12(key) 逻辑左移4位, 左移相等于 *16(bucket大小), 又由于key相当于bucket在bucket中的下标, 所以这一步就是 计算出内存偏移量
// p10 是buekets的首地址, 与偏移量相加 ,计算出bucket 实际的内存地址, 存到p13
// p13 就是buckets中下标为key的元素的 地址
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//ldp 出栈指令(`ldr` 的变种指令,可以同时操作两个寄存器)
//将x13偏移BUCKET_SIZE (16) 个字节的内容取出来, 分别存入x17 和 x9
//p17是imp p9是sel
//然后将bucket指针前移一个单位长度
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
//比较sel和cmd 比较从buckets中取出的sel和传入的sel是否相等
	cmp	p9, p1				//     if (sel != _cmd) {
//如果不相等, 就跳转到3执行
	b.ne	3f				//         scan more
						//     } else {
//如果相等 缓存命中 跳转到CacheHit执行
2:	CacheHit \Mode				// hit:    call or return imp
						//     }

//如果p9为0 取出的sel为空 未找到缓存 就跳转到 __objc_msgSend_uncached执行
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
//比较bucket地址和buckets地址
//当bucket >= buckets 还成立时 跳回1 继续执行 哈希探测
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

//再查找一遍
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
//最后强制跳转到cache miss
LLookupRecover\Function:
	b	\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

Copy the code

The flow of CacheLookup is as follows:

  • First, store the original ISA to X15
  • After theclassA series of displacement operations to obtain storage cachebuckets
  • For the currentselPerform the hash calculation to obtain the corresponding subscript key
  • According to the subscript, take it outbucketsThe corresponding cache inbucket“And got itbuckettheselandimp
  • Then there are some judgments:
    • If you take outselIf the value is null, the current value is not cachedselTo jump to__objc_msgSend_uncached
    • If you take outselAnd the currentselIf yes, the cache is hitCacheHit
    • ifselNot empty, and currentselIf it’s equal or not equal, it means it’s a hash collision, maybe there’s still a cache, so go ahead and hash it outkeyBefore the subscriptbucketContinue to judge
  • If I go all the way throughbuckets, still no cache hit, so iteratebucketsDo a lookup. Other threads add methods to the cache that the current thread is calling, possibly because of a multi-thread lock. If you find it this time, jumpCacheHitperform
  • If still not found, this section of code is also executed to the end, skip__objc_msgSend_uncachedThe execution.

4. Slow the search process

4.1 __objc_msgSend_uncached

__objc_msgSend_uncached is used when there is no implementation of the corresponding method in the cached class. The method slow lookup process will start.

__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
	
	/*
 .macro TailCallFunctionPointer
     // $0 = function pointer value
     braaz    $0
 .endmacro
 */
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
Copy the code

MethodTableLookup

SAVE_REGS MSGSEND // lookUpImpOrForward(obj, SEL, CLS, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) / / receiver and the selector already in x0 and x1 / / / / displacement for call _lookUpImpOrForward method parameters // pass class as the third argument mov x2, x16 // pass the fourth argument 3 mov x3, #3 //bl bl // if not found in the cache, Go to _lookUpImpOrForward (c function) to find the function in the list of methods. Bl _lookUpImpOrForward // IMP in X0 mov x17, x0 // restore register and return RESTORE_REGS msgsend.endmacroCopy the code

__objc_msgSend_uncached has two parts: execute MethodTableLookup and TailCallFunctionPointer. TailCallFunctionPointer is a jump instruction macro definition that jumps to the address of the method implementation to execute. So the way to find a method is just to look at the method table.

MethodTableLookup Main execution process:

  • Saves the parameter to the register, is_lookUpImpOrForwardExecution preparation
    • X0 Message Receiver (self)
    • x1 selector
    • x2 class
    • x3 3
  • Jump tolookUpImpOrForwardFunction to perform a slow lookup
  • lookUpImpOrForwardThe return value of the method is stored inx0That will bex0The register value is stored tox17, will jump tox17Address to execute

4.2 lookUpImpOrForward

The main function of lookUpImpOrForward is to look for IMPs from the list of methods in the CLS and, if not, to the parent (including the parent’s cache and list of methods).

  • If we find it,imp, stored toclassthecache_t, so that it can be quickly found the next time it is called, and willimpreturn
  • If you don’t find itimp, and conduct the behavior (behavior) :
    • ifbehaviorIf methodological decisions are allowed, they are carried outresolveMethod_locked) and will decide the outcomereturn
    • ifbehaviorMethod resolutions are no longer allowed and continue down
  • Done: The code at the tag willimpRecord and cache tocache_tIn the
    • At this timeimpMay be the real method implementation
    • It can also be an alternative to assignment because no real method implementation has been foundforward_imp
  • Execution result
    • May return the actual method implementation address
    • Might return nil
    • May returnforward_imp
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, Int behavior) {//1. Forward_imp // objc_defaultForwardHandler const IMP forward_IMP = (IMP)_objc_msgForward_impcache; // temporary variable IMP IMP = nil; Class curClass; runtimeLock.assertUnlocked(); If (slowpath(!) slowpath(!); CLS - > isInitialized ())) {/ / | = with operation And will result the assignment behaviors | = LOOKUP_NOCACHE; } //2.2 runtimeLock is held during method lookups and caches to keep method additions atomic. runtimelock.lock (); CheckIsKnownClass (CLS); / / 2.4 check whether the class is implemented, did not realize it / / check whether the class is initialized, not initialized as CLS = realizeAndInitializeIfNeeded_locked (inst, CLS. behavior & LOOKUP_INITIALIZE); // Make sure that 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 that this is a miss most of the time, hence a time loss. The evidence suggests that most of the time this is a miss and therefore a waste of time. // The only codepath calling into this without having performed some // kind of cache lookup is // The only code path that does not perform some kind of cache lookup is class_getInstanceMethod(). UnreasonableClassCount () for (unsigned attempts = unreasonableClassCount();) { 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(); #endif } else { // curClass method list. Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { imp = meth->imp(false); goto done; } if (slowpath((curClass = curClass->getSuperclass()) == nil)) { // No implementation found, and method resolver didn't help. // Use forwarding. imp = forward_imp; break; } } // Halt if there is a cycle in the superclass chain. if (slowpath(--attempts == 0)) { _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; } if (fastpath(imp)) { // Found the method in a superclass. Cache it in this class. goto done; }} No implementation found. Try method resolver once. Slowpath (behavior & LOOKUP_RESOLVER)) {//LOOKUP_RESOLVER = 2 behavior = 3 // Behavior xor = 1 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: // unlock runtimelock.unlock (); // return imp if (slowpath((behavior & LOOKUP_NIL) && IMP == forward_imp)) {return nil; } return imp; }Copy the code

Forward_imp is actually objc_defaultForwardHandler, a C method. When no real IMP is found, the address of objc_defaultForwardHandler is returned and the call is made. XXX unrecognized selector sent to instance XXX.

__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

The traversal search process is roughly as follows:

  1. Select * from curClass’s list of methods. CLS ->data()->methods() 1.1 If imPs is found, skip to done and continue. 1.2 If imPs is not found, curClass points to parent of curClass, ready to look up. Note after traversing all parent classes are not found imp, the loop ends, break

  2. Imp =forward_imp; if imp is not empty, but imp= forward_IMP, The parent class failed to find sel imp, so it can only put forward_IMP into the cache. In this case, it is not necessary to continue searching, because it can not find sel IMP. End the loop, break 2.2 If imp is not null and is not equal to forward_IMP, then imp is found in the parent class. Skip to the done mark and continue the loop. Go to the list of methods in the curClass and start the next iteration of the search.

  3. If the second pass does not find it, it goes to the cache of the parent of the curClass, and the cycle continues until it ends (found or not found). If it finds the “done” flag, it will proceed to the method resolution judgment if it does not.

The judgment of method resolution is as follows:

If (slowpath(behavior & LOOKUP_RESOLVER)) {//LOOKUP_RESOLVER = 2 behavior = 3 // slowpath(behavior & LOOKUP_RESOLVER) = 1 LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); }Copy the code

When no method implementation is found, a method resolution is tried. It is called once because behavior=3 when the lookUpImpOrForward method is called from the methodTable Ookup assembly code. So when we go inside if, we do xor to behavior, and then behavior=1. When the resolveMethod_locked method is resolved, lookUpImpOrForward is tried again, and behavior=1 cannot be executed in if. Will execute down done:

Done: The execution of the tag will also have two cases:

  1. Through the for loop above, we find the implementation of the method and jump directly to done execution
  2. Through the for loop above, no implementation of the method is found, the method resolution is performed, and the method resolution is executed againlookUpImpOrForwardMethod, still haven’t found a way to implementimpAs mentioned earlier, method resolutions are executed only once, so instead of going to method resolutions, they are executed downwarddone:

2, imp= forward_IMP, forward_IMP will be stored in the CLS cache.

LookUpImpOrForward, the slow lookup process of the lookUpImpOrForward method.

5. To summarize

Method execution flow:

  • So let’s go to the cache and find the execution
  • If you can’t find it in the cache, go through the list of methods to find the implementation
  • Not found in current class method list, found in parent class (cache and method list), found to execute
  • Still not found, method resolution, enter the forwarding process, message processing, execution
  • Finally still not processed, crash