[iOS] Messaging and message forwarding mechanism

message passing mechanism

In the OC language, calling the method of an object is called message passing. A message has a name and a selector, can accept parameters, and may have a return value.

In Objective-C, if a message is passed to an object, the dynamic binding mechanism is used to determine the method that needs to be called. At the bottom level, all methods are ordinary C language functions. However, after the object receives the message, which method should be called is completely determined during the runtime, and can even be changed while the program is running. These features make Objective-C a real programming language. dynamic language.

Example: OC message expression:

id returnValue = [someObject messageName:parameter];

This section will be processed by the compiler into:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

someObject is called the receiver, messageName is called the "selector", and the selector and parameters together are called the "message". When the compiler sees this message, it will be converted into a standard C language function call.

SELECT SEL

During compilation, OC will generate a unique ID to distinguish this method based on the name of the method (including parameter sequence). This ID is of SEL type. What we need to note is that as long as the method names (including parameter sequences) are the same, their IDs are the same. So whether it is a parent class or a subclass, if the name is the same, the ID will be the same.

	SEL sell1 = @selector(eat:);
    NSLog(@"sell1:%p", sell1);
    SEL sell2 = @selector(eat);
    NSLog(@"sell2:%p", sell2);
    //sell1:0x100000f63
	//sell2:0x100000f68

This mechanism greatly increases the flexibility of our program. We can pass SEL parameters to a method and let the method dynamically execute a certain method; we can also specify the method that needs to be executed through the configuration file, and the program reads the configuration. The file then translates the method string into a SEL variable and then sends the message to the corresponding object.

From an efficiency point of view, when executing, the method is searched not by the method name but by the method ID, which is an integer. Since the search and matching of integers is much faster than that of strings, this can improve execution to some extent. s efficiency

We need to note that @selector is equivalent to translating the method name into the SEL method name. It only cares about the method name and the number of parameters, and does not care about the return value and parameter type.

The process of generating SEL is fixed, because it is just an ID indicating the method. No matter which class the dayin method is written in, the SEL value is fixed.

A SEL table is maintained in Runtime. This table stores SELs not according to classes. As long as the same SELs are regarded as one, they will be stored in the table. When the project is loaded, all methods will be loaded into this table, and dynamically generated methods will also be loaded into the table.

Then different classes can have the same method. When instance objects of different classes execute the same selector, they will find the IMP corresponding to their own class based on SEL in their respective method lists.

IMP is essentially a function pointer. The pointed function contains an object ID that receives the message, the SEL for calling the method, and some method parameters, and returns an ID. Therefore, we can obtain its corresponding IMP through SEL. After obtaining the function pointer, it means that we have obtained the code entry that needs to execute the method, so that we can use this function pointer like a normal C language function call.

objc_msgSend

We can see that the objc_msgSend function is used in the conversion. This function takes the message receiver and method name as the main parameters, as shown below:

objc_msgSend(receiver, selector)                    // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...)    // 带参数

objc_msgSend implements the dynamic binding mechanism through the following steps:

  • First, get the method implementation pointed to by the selector. Since the same method may have different implementations in different classes, the judgment is based on the class to which the receiver belongs.
  • Secondly, pass the receiver object and the parameters specified by the method to call the method implementation.
  • Finally, the return value of the method implementation is returned.

The key to message passing lies in the objc_class structure, which has three key fields:

  • isa: Pointer to class.
  • superclass: pointer to the parent class.
  • methodLists: The method distribution table of the class (dispatch table).

When a new object is created, memory is allocated for it and its member variables are initialized. The isa pointer will also be initialized, allowing the object to access the class and the class's inheritance chain.

The following figure shows a schematic diagram of the message passing process:

Insert image description here

  • When a message is delivered to an object, it is first looked up from the runtime system cache objc_cache. If found, execute. Otherwise, continue with the following steps.
  • objc_msgSend obtains the class structure through the isa pointer of the object, and then searches the method selector in the method distribution table methodLists. If it is not found, its parent class will be found along the superclass of the class, and the search will continue in the distribution table methodLists of the parent class.
  • By analogy, the inheritance chain of the class is traced back to the NSObject class. Once the selector is found, pass in the corresponding parameters to execute the specific implementation of the method, and add the method to the cache objc_cache. If the selector is still not found in the end, the message forwarding process will be entered.

Source code analysis

quick search

objc_msgSend is implemented in different architectures: taking arm64 as an example, the code implementation is assembly. Why choose assembly to implement? It is faster, uses parameters directly, and avoids the overhead of copying a large number of parameters. An underscore "_" will be added in front of functions and global variables to prevent symbol conflicts.

Assembly process:

	//进入objc_msgSend流程
	ENTRY _objc_msgSend
    //流程开始,无需frame
	UNWIND _objc_msgSend, NoFrame

    //判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend
	cmp	p0, #0			// nil check and tagged pointer check
	//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS
    //b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
    //等于,如果不支持小对象,就跳转至LReturnZero退出
	b.eq	LReturnZero
#endif
    //通过p13取isa
	ldr	p13, [x0]		// p13 = isa
    //通过isa取class并保存到p16寄存器中
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
  • First, start with cmp p0,#0, where p0 is a register that stores the message recipient. When entering the message sending entry, first determine whether the message receiver exists. If not, re-execute objc_msgSend.
  • b.le LNilOrTagged, b means jump to. le means if p0 is less than or equal to 0, the overall meaning is that if p0 is less than or equal to 0, jump to LNilOrTagged, execute b.eq LReturnZero to exit this function directly
  • If the message receiver is not nil, the assembly continues to run, goes to CacheLookup NORMAL, and looks for imp in the cache.

Let’s take a look at the specific implementation:

//在cache中通过sel查找imp的核心流程
.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.
	//
	//   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:
	//
	//   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
	//

    //从x16中取出class移到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
    //ldr表示将一个值存入到p10寄存器中
    //x16表示p16寄存器存储的值,当前是Class
    //#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
    //#define CACHE (2 * __SIZEOF_POINTER__)
    //经计算,p10就是cache
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
    //and表示与运算,将与上mask后的buckets值保存到p10寄存器
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
    //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
	tbnz	p11, #0, LLookupPreopt\Function
#endif
    //按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
    //LSR表示逻辑向右偏移
    //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
    //这个是哈希算法,p12存储的就是搜索下标(哈希地址)
    //整句表示_cmd & mask并保存到p12
	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

    //去除掩码后bucket的内存平移
    //PTRSHIFT经全局搜索发现是3
    //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
    //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
    
    
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
    //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
	cmp	p9, p1				//     if (sel != _cmd) {
    
    
    //b.ne表示如果不相同则跳转到3f
	b.ne	3f				//         scan more
						//     } else {
    
    
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
    //通过p13和p10来判断是否是第一个bucket
	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:
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

	// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)
	// keep the remaining 38 bits for the IMP offset, which may need to reach
	// across the shared cache. This offset needs to be shifted << 2. We did this
	// to give it even more reach, given the alignment of source (the class data)
	// and destination (the IMP)
	ldr	x17, [x10, x9, LSL #3]		// x17 == (sel_offs << 38) | imp_offs
	cmp	x12, x17, LSR #38

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
	sub	x0, x16, x17        		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				        // cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
	sub x17, x16, x17               // 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

1. Process:

  1. Get pointers to cache and _bucketsAndMaybeMask;
  2. Take the buckets and mask respectively from _bucketsAndMaybeMask, and use the mask to calculate the hash subscript according to the hash algorithm;
  3. According to the obtained hash subscript begin and buckets first address, extract the bucket corresponding to the hash subscript;
  4. Enter the do-while loop and search based on sel in the bucket;

The cache and _bucketsAndMaybeMask are obtained through memory translation. The high 16 bits of _bucketsAndMaybeMask store the mask, and the low 48 bits store buckets (high 16 bits | low 48 bits = mask | buckets), that is, _bucketsAndMaybeMask = mask (high 16) + buckets pointer (low 48 bit).

Using the parameter p1 (that is, the second parameter _sel) & mask of objc_msgSend, through the hash algorithm, we get the bucket subscript begin that needs to be found to store sel-imp, that is, p12 = begin = _sel & mask, because when storing sel-imp When , the hash subscript is also calculated and stored through the same hash algorithm.

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    
    
    return (mask_t)(uintptr_t)sel & mask;
}

According to the calculated hash subscript begin multiplied by the memory size occupied by a single bucket, the offset in the actual memory of the bucket's first address from the bucket pointed to by the begin subscript is obtained. Get the bucket corresponding to hash subscript begin through the first address + actual offset. Bucket is composed of two attributes: sel and imp. Each attribute is 8 bytes in size, so the bucket size is 16

In the do-while loop:

  • In the first do-while loop, search from begin —> 0. If there is no hit, p9 is not nil, and the second do-while loop starts;
  • In the second do-while loop, search again from mask —> 0;
  • If this is still the case, execute __objc_msgSend_uncached —> MethodTableLookup —> _lookUpImpOrForward to start looking for the method list.

If you find a method in the cache, call it directly. If you find sel, you will enter CacheHit and return or call imp: Return or call the implementation of the method (imp).

2. Contents of CacheHit: Mode in the picture above represents the following NORMAL process, authenticate and call imp means verification and calling method implementation.

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp//调用imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP//返回imp
.elseif $0 == LOOKUP// 执行__objc_msgSend_uncached,开始方法列表查找
	// 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 oops
.endif
.endmacro

3. Method not found in cache

If the cache is not found, search for the next bucket and loop until the corresponding method is found. If the method is not found after the loop, call __objc_msgSend_uncached.

The following is the above judgment jump code:

//LGetIsaDone是一个入口
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
    //进入到缓存查找或者没有缓存查找方法的流程
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

__objc_msgSend_uncached source code assembly:

	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

The MethodTableLookup macro is called: to find a method from the method list.

Take a look at its structure:

	.macro MethodTableLookup
	
	SAVE_REGS MSGSEND

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

.endmacro

Among them, bl means that the method _lookUpImpOrForward is called. _lookUpImpOrForward cannot be found in the assembly, because the assembly function has one more underscore than the C++ one. You need to remove the underscore to find the lookUpImpOrForward method implementation.

At this point, the fast search imp assembly part is over, and then comes the slow search process: c/c++ link.

Summary message sent quick lookup imp (assembly):

objc_msgSend(receiver, sel, …)

  1. Check whether the message receiver exists. If it is nil, no processing will be done.
  2. Find the corresponding class object through the isa pointer of the receiver
  3. Find the class object for memory translation and find the cache
  4. Get buckets from cache
  5. Compare the parameter sel from the buckets to see if there is a method with the same name in the cache.
  6. If there is a corresponding sel in the buckets --> cacheHit --> call imp
  7. If there is no corresponding sel in the buckets --> _objc_msgSend_uncached -> _lookUpImpOrForward (c/c++ slow search)

slow search

method buffer

Apple believes that if a method is called, that method has a greater chance of being called again. In this case, it directly maintains a cache list, loads the called method into the cache list, and when calling the method again, cache it first. Search in the list, and if you can't find it, go to the method list to query. This avoids having to go to the method list to query every time a method is called, which greatly improves the speed.

Search process

Let’s first look at the implementation of the lookUpImpOrForward function:

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    
    
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    if (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 flush on
    // behalf of the category.

    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.
    // 检查当前类是个已知类
    checkIsKnownClass(cls);
    // 确定当前类的继承关系
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE); 
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    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().

    for (unsigned attempts = unreasonableClassCount();;) {
    
    
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel); //cache中找IMP
            if (imp) goto done_unlock; //找到就直接返回了
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
    
     //如果不是常量优化缓存
            // 当前类的方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
    
    
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    
    
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        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)) {
    
    
            // 在超类中找到方法。在这个类中缓存它。
            goto 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 // iOS操作系统且真机的情况下
        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;
}

The method is to first define a message forwarding forward_imp; then determine the initialization of the class, lock it, check whether it is a known class...etc., ignore these for now. The focus is on the following for loop:

// unreasonableClassCount()表示循环的上限;
    for (unsigned attempts = unreasonableClassCount();;) {
    
    
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
    
    
            // curClass方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
    
    
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    
    
                // 没有找到实现,方法解析器没有帮助。
                // 使用转发。
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        if (slowpath(--attempts == 0)) {
    
    
            _objc_fatal("Memory corruption in class list.");
        }

        // 超类缓存。
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
    
    
            // 在超类中找到forward::条目。
            // 停止搜索,但不要缓存;调用方法
            // 首先为这个类解析器。
            break;
        }
        if (fastpath(imp)) {
    
    
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

Entering a circular logic:

  • Find imp from the method list of this class (the search method is getMethodNoSuper_nolock, which will be analyzed later);
  • Find imp from the cache of the parent class of this class (written by cache_getImp assembly)
  • Find imp from the method list of the parent class of this class
  • …inheritance chain traversal…(parent class->…->root parent class)
  • If imp is found in any of the above links, the loop will be jumped out, and the cache method will be cached in the cache of this class (log_and_fill_cache);
    until nil is found, imp will be designated as message forwarding, and the loop will be jumped out of.

Search method

Take a look at how to find imp in the class and parent class inheritance chain (getMethodNoSuper_nolock):

/***********************************************************************
 * 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;
}

Jump search_method_list_inline()

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    
    
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    // 已排序的二分查找
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
    
    
        return findMethodInSortedMethodList(sel, mlist);
    } else {
    
    
        // Linear search of unsorted method list
      	// 未排序的线性查找
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
    
    
        for (auto& meth : *mlist) {
    
    
            if (meth.name() == sel) {
    
    
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

fastpath() represents the approximate path that will be taken. The following are searches in two situations.

  1. findMethodInSortedMethodList: From Sorted, it can be seen that searching from the sorted method list uses binary search.
  2. findMethodInUnsortedMethodList: It can be seen from Unsorted that a linear search is used in the unsorted method list, and method_t is taken out by traversing the comparison sel one by one through a for loop:.

Take a look at the findMethodInSortedMethodList function, jump to findMethodInSortedMethodList, ALWAYS_INLINE means this is always inline.

// 方法内联
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    
    
    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 {
    
    
        return findMethodInSortedMethodList(key, list, [](method_t &m) {
    
     return m.big().name; });
    }
}

After compilation, the following process is followed, which is method search through binary search.

/***********************************************************************
 * 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 代表自动匹配类型;
    auto first = list->begin();
    auto base = first;
  	// decltype: declare type,译为声明类型。这里获取表达式类型;
    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;
}

The function gets the method name of the probe and compares it with the passed in key. If they are equal, it means that a matching method was found. The function then backtracks to find the first occurrence of the method name in the method list to handle the category's overriding of the method.

After jumping out of the loop

done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    
    
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
        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;

If imp is found, imp will be cached in this type of cache (log_and_fill_cache). (Note that whether this class or the parent class of this class finds imp, it will be cached in this class)

Jump to 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;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}

This code is used to record message logs when methods are called and cache frequently called methods to speed up subsequent calls to the method.

Summary message sent slow lookup imp(c/c++): IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

  1. Find imp from the method list (binary search/traversal search) of this class;
  2. Find imp (assembly) from the cache of the parent class of this class;
  3. Find imp from the method list (binary search/traversal search) of the parent class of this class;
  4. ...Inheritance chain traversal...(parent class->...->root parent class) looks for cache and method list imp;
  5. If imp is found in any of the above links, jump out of the loop, cache the method to the cache of this class, and return imp;
  6. Until nil is found, specify imp as message forwarding, jump out of the loop, and perform dynamic method resolution resolveMethod_locked.

message forwarding

dynamic resolution

As introduced above, the essence of method calling is message sending. If no method is found after searching, what will the system do? This is the method of dynamic resolution and message forwarding introduced next.

dynamic decision process

cacheWhen neither this class nor the sum under the inheritance chain of this class method listcan be found imp, and it impis assigned _objc_msgForward_impcachebut it is not called, it will enter the dynamic method resolution process, and it will only be executed once.

resolveMethod_lockedSource code statement:

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    
    
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    //判断是不是元类
    if (! cls->isMetaClass()) {
    
    
        // 不是元类,则是实例方法的动态方法解析
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
    
    
        // 是元类,则是类方法的动态方法解析
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls); // inst:类对象   cls: 元类
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    
    
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

This code is the function used to resolve the method resolveMethod_locked. It performs dynamic method resolution based on the type of the class (metaclass or non-metaclass) and attempts to find the method implementation from the cache.

The main logic of the function is as follows:

  1. First, the function asserts that the runtime lock runtimeLockis already held to avoid contention during the call.
  2. Next, the function checks whether the class has been instantiated (realized). If the class has not been instantiated, then it will not have any methods to parse and return directly.
  3. The function then releases the runtime lock, allowing other threads to access the runtime.
  4. According to the type of class, dynamic analysis of different types of methods is performed:
    • If the class is not a metaclass, it means resolving instance methods. Call [cls resolveInstanceMethod:sel]the try resolution method.
    • If the class is a metaclass, it means parsing the class method. The try-resolve method is called first [nonMetaClass resolveClassMethod:sel], and if it's not found, it continues by calling [cls resolveInstanceMethod:sel]the try-resolve instance method.
  5. After parsing the method, the function attempts to find the method implementation from the cache. If the method implementation can be found from the cache, the implementation is returned directly. Otherwise, the function will be further processed according to the given behavior behavior, possibly method forwarding or finding the parent class method implementation.

Two methods: resolveInstanceMethodand resolveClassMethod. Also called dynamic resolution of a method.

After executing the above code, return lookUpImpOrForwardTryCache:

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    
    
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

This method calls _lookUpImpTryCachethe method:

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    
    
    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
    
    
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
    
    
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) {
    
    
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
    
    
        return nil;
    }
    return imp;
}

Entering _lookUpImpTryCachethe source code, you can see here cache_getImp; that is to say, after a dynamic resolution, the sel of the method will be searched from the cache through cache_getImp.

If still not found (imp == NULL)? That is to say, if the method cannot be added dynamically, it will be executed once lookUpImpOrForward. When the method is entered , the value passed lookUpImpOrForwardhere will change.behavior

After entering lookUpImpOrForwardthe method for the second time, when this judgment is executed if (slowpath(behavior & LOOKUP_RESOLVER)):

// 这里就是消息转发机制第一层的入口
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    
    
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

According to the changed behaviorvalue and LOOKUP_RESOLVERthe relationship between the values, the if statement can only be entered for the first time, so this judgment is equivalent to a singleton. It explains why the dynamic analysis mentioned at the beginning resolveMethod_lockedis only executed once.

Dynamic parsing test

resolveClassMethod: The default return value is NO. If you want to add a method implementation to this function, you need to use class_addMethod

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

@cls : 给哪个类对象添加方法
@name : SEL类型,给哪个方法名添加方法实现
@imp : IMP类型的,要把哪个方法实现添加给给定的方法名
@types : 就是表示返回值和参数类型的字符串

To implement a class, the class declares a method in the .h file, but this method is not implemented in the .m file. Calling this method externally will cause the program to crash

reason:

  1. In the first step of the search method, the implementation of this method was not found in either the own class object or the class object of the parent class.
  2. So moving to dynamic method parsing, dynamic method parsing we did nothing,
  3. So we proceed to the third step, turning to message forwarding. We did nothing in message forwarding, and finally crashed.

At this point we remedy it in the dynamic method parsing step:

  • When an object method is called, dynamic method resolution is resolveInstanceMethodimplemented in the method
  • When a class method is called, dynamic method resolution is resolveClassMethodimplemented in

Using dynamic method parsing and summing runtime, we can add method implementation to an unimplemented method.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface person : NSObject
- (void)test;
@end

NS_ASSUME_NONNULL_END

#import "person.h"
#import <objc/message.h>
#import <objc/runtime.h>

@implementation person
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    
    NSLog(@"%s, self = %@", __func__, NSStringFromSelector(sel));
    return  [super resolveInstanceMethod:sel];
}
@end

Run as follows:
Insert image description here

You can see why there are 2 executions? Let’s leave it to the end. The same goes for class methods.

Since it crashed because it could not be found , we can pass impit in this method and generate it dynamically . The fourth parameter is the return value type, described by a string:runtimeclass_addMethodselimpvoid“v@:”

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    
    
    if (!cls) return NO;

    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

Method modification:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    
    NSLog(@"%s, self = %@", __func__, NSStringFromSelector(sel));
    if (sel == @selector(test)) {
    
    
        IMP imp = class_getMethodImplementation(self.class, @selector(addMethod));
        class_addMethod(self.class, sel, imp, "v@:");
    }
    return  [super resolveInstanceMethod:sel];
}

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

You can see that it is running normally:
Insert image description here

message forwarding

If the system does not find an implementation during the dynamic resolution phase, it will enter the message forwarding phase.

Fast message forwarding

When cachenot found imp, the method list in the inheritance chain of the class is not found imp, and resolveInstanceMethod / resolveClassMethodNO will be returned to enter message forwarding.

When we were in lookUpImpOrForward, we saw impthat was designated _objc_msgForward_impcache.

	//如果上述在类对象和父类对象中没有查到方法
	//我们就进入动态方法解析
 if (resolver  &&  !triedResolver) {
    
    //triedResolver用来判断是否曾经进行过动态方法解析,如果没有那就进入动态方法解析,如果进行过,就跳过
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst); //动态方法解析函数
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES; //进行过动态方法解析就把这个标识为设置为YES
       goto retry;//retry是前面的发送消息的过程
    }
    	
      //如果动态方法解析失败,就进入消息转发

    imp = (IMP)_objc_msgForward_impcache; //由这一步进入消息转发
    cache_fill(cls, sel, imp, inst);
//如果消息转发失败,程序崩溃
 done:
    runtimeLock.unlock();

So if this class does not have the ability to process this message, then it will be forwarded to other classes and let other classes handle it.

Let’s take a look at the specific implementation of the message forwarding function __objc_msgForward_impcache. It is the process of message forwarding; it’s time for our source code assembly stage again:

 STATIC_ENTRY __objc_msgForward_impcache
	// Method cache version

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band condition register is NE for stret, EQ otherwise.

	jne	__objc_msgForward_stret
	jmp	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache
	
	
	ENTRY __objc_msgForward
	// Non-stret version

	movq	__objc_forward_handler(%rip), %r11
	jmp	*%r11

	END_ENTRY __objc_msgForward

But __objc_forward_handler is not open source.

Message fast forwarding test

  1. The func1 method is defined in the Person class but is not implemented. Use the -(id)forwardingTargetForSelector:(SEL)aSelector method for fast message forwarding.
  2. Define func1 method in Blank class and implement it
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
- (void)func1;
@end

NS_ASSUME_NONNULL_END

#import "Person.h"
#import "Blank.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Person

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    
    NSLog(@"%s, aSelector = %@", __func__, NSStringFromSelector(aSelector));
    
    if (aSelector == @selector(func1)) {
    
    
        return [Blank alloc];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
@end

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Blank : NSObject
- (void)func1;
@end

NS_ASSUME_NONNULL_END

#import "Blank.h"

@implementation Blank
- (void)func1 {
    
    
    NSLog(@"%s", __func__);
}
@end

main.m file, create a new person object and call the func1 method

#import <Foundation/Foundation.h>
#import "Person.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        Person* person = [[Person alloc] init];
        [person func1];
    }
    return 0;
}

Run as follows:
Insert image description here

The function of forwarding is that if the current object cannot respond to the message, it is forwarded to an object that can respond.

Where is the method cache at this time? Object that receives forwarded messages

Application scenario: Create a dedicated class to handle these unresponsive messages. Crash collection when the method cannot be found.

The demonstration is an instance method. If it is a class method, just change - to +;

Slow forwarding of messages

If no method is found for fast forwarding of messages; there is another methodSignatureForSelectormethod later, which is used for method validity signature. It also needs to be paired with another method:forwardInvocation

forwardInvocationThe method provides an input parameter of type NSInvocation; it provides targetand selectoris used to find the method implementation in the specified target.

After commenting on the fast forwarding method just used forwardingTargetForSelector, add methodSignatureForSelectorthe method and forwardInvocationmethod.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    
    NSLog(@"%s, aSelector = %@", __func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    
    
}

Operating normally:
Insert image description here

Summarize

The essence of OC method calling is message sending, and message sending is the search process of SEL-IMP.

dynamic resolution

  • There is no way to find it through the message sending mechanism. The system will also perform dynamic resolution before entering the message forwarding.

Dynamic resolution of instance methods

+ (BOOL)resolveInstanceMethod:(SEL)sel;
// 系统通过该方法调用上面OC类里的实现
static void resolveInstanceMethod(id inst, SEL sel, Class cls) 

Dynamic resolution of class methods

+ (BOOL)resolveClassMethod:(SEL)sel;

message forwarding

  • Dynamic resolution can't find a way to really enter the message forwarding stage.
  • Dynamic resolution, fast forwarding, and slow forwarding are collectively called the three life-saving straws, used to prevent system crashes caused by method search.

Fast message forwarding

- (id)forwardingTargetForSelector:(SEL)aSelector;

Message forwarded slowly

// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 正向调用
- (void)forwardInvocation:(NSInvocation *)anInvocation;

The message forwarding mechanism is basically divided into three steps, also known as the three rescues of messages:

  1. Dynamic method parsing
  2. backup receiver
  3. Complete message forwarding

We can solve this problem by controlling one of these three steps

Special note: If it is a normal message, it will not go through these three steps. Therefore, the prerequisite for reaching these three steps is to determine that the message is an unknown message.

flow chart

Insert image description here

some problems

How does the runtime find the corresponding IMP address through the selector?

Cache search–>Current class search–>Parent class search level by level

If a subclass calls a parent class method, which class is cached in?

When a child class calls a method of the parent class, the Objective-C runtime will first check the child class's method cache. If the method of the parent class already exists in the method cache of the child class, the implementation of the method is obtained directly from the cache and called. This improves the speed of method lookup and avoids the need for a complete method lookup on every call.

If the method of the parent class does not exist in the method cache of the subclass, the method implementation will continue to be searched upward. First the cache of the parent class is looked up, then the parent class's parent class, and so on until the implementation of the method is found or the top of the inheritance chain is reached.

It should be noted that if the subclass overrides the method of the parent class and calls the super keyword in the subclass to call the method of the parent class, then this method call will not use the cache, but will be determined through a complete method lookup. implementation of the method.

To sum up, when a subclass calls a method of a parent class, the cache is stored in the method cache of the subclass, but if the subclass overrides the method of the parent class and uses super to call the method of the parent class, the cache will not be used.

Reasons for two dynamic resolutions

Test with breakpoints:
Insert image description here

After running, you can see the printed information by entering the command bt in lldb.
When you enter the breakpoint for the first time and enter bt, the display is as follows:
Insert image description here

The second time you enter the breakpoint and enter bt, the display is as follows:
Insert image description here

The symbol is called ___forwarding___, and the familiar slow forwarding methodSignatureForSelectormethod is used. It can be seen that the second time is message forwarding;

After the first dynamic resolution and fast forwarding of the message failed to find a solution, it entered slow forwarding. During the process, runtimeit will be called once lookUpImpOrForward. This method contains dynamic resolution, which results in a second dynamic resolution.

Guess you like

Origin blog.csdn.net/m0_63852285/article/details/131024005