当前位置: 首页 > news >正文

【iOS】—— 消息传递和消息转发

【iOS】—— 消息传递和消息转发

    • 1. 消息传递
      • SEL选择子
      • IMP
      • 快速查找
        • 汇编代码查找过程
        • 总结消息转送快速查找IMP
      • 慢速查找
        • 总结消息传递慢速查找IMP
    • 2. 消息转发
      • 动态决议
        • 动态解析添加方法
      • 快速转发
      • 慢速转发
    • 总结
      • 动态决议
      • 消息转发
      • 消息的三次拯救

1. 消息传递

在iOS中,消息传递机制是基于Objective-C语言的动态性质的一种编程方式。这个概念主要涉及两个概念:发送者(消息发送的对象)和接受者(消息接收的对象)。当调用一个对象的方法的时候,实际上是向这个对象发送了一条消息。

比如下面的代码:

id returnValue = [someObject messageName: parameter];

someObject叫做接收者(receiver),messageName:叫做选择子(selector),选择子和参数合起来称为“消息”。

编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,

编译器看到上述这条消息会转换成一条标准的 C 语言函数调用:

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

objc_msgSend函数,这个函数将消息接收者和方法名作为主要参数,其原型如下所示:

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

objc_msgSend通过以下几个步骤实现了动态绑定机制:

  1. 首先获取selector指向的方法实现。因为相同的方法可能会在不同的类中有不同的实现,所以要根据receiver来进行判断。
  2. 其次,传递对象,方法指定的参数来调用方法实现。
  3. 最后返回方法实现的返回值。
  4. 当消息传递到一个对象的时候,首先从运行时的系统缓存objc_cache中进行查找。如果找到,就执行。否则执行下一步。
  5. objc_msgSend通过对象的isa指针获取类的结构体,然后在结构体的methodLists中查找方法,如果没有找到,就沿着superclass找到父类,在父类的分发表methodLists中继续查找。
  6. 以此类推,一直沿着继承链找到NSObject类。一旦找到selector,传入相应的参数来实现具体方法,并将该方法加入到objc_cache。如果最后还没有找到,就会进入消息转发流程。

SEL选择子

SEL 是选择器(Selector)的别名,它是表示一个方法的符号名。选择器是用来表示一个方法名的,可以看作是一个指向方法的指针。

在OC中方法并不是一个单纯的函数,由两部分组成:选择器(SEL)和实现体(IMP)。

选择器是一个字符串,用来表示方法名字;实现体是一个函数指针,指向方法的实现。

每个方法在 Objective-C 运行时环境中都有一个选择器与之对应。选择器可以看作是一个内部的名称,用于在运行时识别要被调用的方法。你可以通过 @selector() 来获取一个方法的选择器。

例如,假设你有一个名为 doSomething 的方法,你可以这样获取它的选择器:

 SEL selector = @selector(doSomething);

选择器主要用于以下几个方面:

  • 方法的调用:可以通过 -performSelector: 方法和一些变体来间接调用一个方法。这在你需要在运行时动态决定要调用的方法时非常有用。
  • 作为方法的参数:在很多 Cocoa 和 Cocoa Touch 的 API 中,你会发现有许多方法的参数是选择器,例如 NSTimer 的 +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:。
  • 响应者链:在 iOS 的事件处理和图形用户界面编程中,选择器常常被用来确定哪个方法应该被调用来响应一个特定的事件,例如按钮点击等。

选择器是在编译阶段由编译器生成的。编译器会根据方法名(包括参数序列)生成一个唯一的 ID,这个 ID 就是 SEL 类型的。

其中需要注意的是:@selector等于是把方法名翻译成SEL方法名。其仅仅关心方法名和参数个数,并不关心返回值与参数类型

IMP

**IMP是一个函数指针,保存了方法地址。**它是OC方法实现代码块的地址,通过他可以直接访问任意一个方法。免去发送消息的代码,IMP声明:

 typedef id (&IMP)(id,SEL,...);

IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针),调用方法的选标SEL(方法名),以及不定个数的方法参数,并返回一个id。

IMP和SEL的区别与联系:

  • SEL:类方法的指针,相当于一种编号,区别与IMP。
  • IMP:函数指针,保存了方法的地址。

SEL是通过表取对应关系的IMP,进行方法的调用。可以将SEL想象成一个指向方法名的指针,但它并不直接关联方法的实现代码,而是作为查找方法实现(即IMP)的一个标记或键值。

查找 IMP方式大致分为两种:快速查找和慢速查找

快速查找

汇编代码查找过程
  1. 首先从cmp p0,#0开始,这里p0是寄存器,存放的是消息接受者。当进入消息发送入口时,先判断消息接收者是否存在,不存在则重新执行objc_msgSend。

  2. b.le LNilOrTagged,b是跳转到的意思。le是如果p0小于等于0,总体意思是若p0小于等于0,则跳转到LNilOrTagged,执行b.eq LReturnZero直接退出这个函数。

 	//进入objc_msgSend流程ENTRY _objc_msgSend//流程开始,无需frameUNWIND _objc_msgSend, NoFrame//判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSendcmp	p0, #0			// nil check and tagged pointer check
//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS//b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTaggedb.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else//等于,如果不支持小对象,就跳转至LReturnZero退出b.eq	LReturnZero
#endif//通过p13取isaldr	p13, [x0]		// p13 = isa//通过isa取class并保存到p16寄存器中GetClassFromIsa_p16 p13, 1, x0	// p16 = class
  1. 如果消息接受者不为nil,汇编继续跑,到CacheLookup NORMAL,在cache中查找imp,来看一下具体的实现
 //在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就是cacheldr	p10, [x16, #CACHE]				// p10 = mask|bucketslsr	p11, p10, #48			// p11 = maskand	p10, p10, #0xffffffffffff	// p10 = bucketsand	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\Functionand	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else//and表示与运算,将与上mask后的buckets值保存到p10寄存器and	p10, p11, #0x0000fffffffffffe	// p10 = buckets//p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopttbnz	p11, #0, LLookupPreopt\Function
#endif//按位右移7个单位,存到p12里面,p0是对象,p1是_cmdeor	p12, p1, p1, LSR #7and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#elseand	p10, p11, #0x0000ffffffffffff	// p10 = buckets//LSR表示逻辑向右偏移//p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask//这个是哈希算法,p12存储的就是搜索下标(哈希地址)//整句表示_cmd & mask并保存到p12and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4ldr	p11, [x16, #CACHE]				// p11 = mask|bucketsand	p10, p11, #~0xf			// p10 = bucketsand	p11, p11, #0xf			// p11 = maskShiftmov	p12, #0xfffflsr	p11, p12, p11			// p11 = mask = 0xffff >> p11and	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,走CacheHitcmp	p9, p1				//     if (sel != _cmd) {//b.ne表示如果不相同则跳转到3fb.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来判断是否是第一个bucketcmp	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_ADDRSadd	p13, p10, w11, UXTW #(1+PTRSHIFT)// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))// p13 = buckets + (mask << 1+PTRSHIFT)// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4add	p13, p10, p11, LSL #(1+PTRSHIFT)// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endifadd	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 hitcmp	p9, #0				// } while (sel != 0 &&ccmp	p13, p12, #0, ne		//     bucket > first_probed)b.hi	4bLLookupEnd\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 = bucketsautdb	x10, x16			// auth as early as possible
#endif// x12 = (_cmd - first_shared_cache_sel)adrp	x9, _MagicSelRef@PAGEldr	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_shiftlsr	x17, x11, #55			// w17 = (hash_shift, ...)lsr	w9, w12, w17			// >>= shiftlsr	x17, x11, #60			// w17 = mask_bitsmov	x11, #0x7ffflsr	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_shiftlsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)lsr	w9, w12, w17			// >>= shiftand	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_offscmp	x12, x17, LSR #38.if \Mode == GETIMPb.ne	\MissLabelConstant		// cache misssbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2sub	x0, x16, x17        		// imp = isa - imp_offsSignAsImp x0ret
.elseb.ne	5f				        // cache misssbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2sub x17, x16, x17               // imp = isa - imp_offs
.if \Mode == NORMALbr	x17
.elseif \Mode == LOOKUPorr x16, x16, #3 // for instrumentation, note that we hit a constant cacheSignAsImp x17ret
.else
.abort  unhandled mode \Mode
.endif5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offsetadd	x16, x16, x9			// compute the fallback isab	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES.endmacro

类对象/元类通过内存平移获得cache,获得buckets。

在缓存中找到了就直接调用,找到sel就会进入CacheHit,去return or call imp:返回或调用方法的实现(imp)。

  1. 如果没有找到缓存,查找下一个bucket,一直循环直到找到对应的方法,最后还没有找到的话,就调用objc_msgSend_uncached方法。

下面是上述判断跳转代码:

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

__objc_msgSend_uncached源码汇编:

 	STATIC_ENTRY __objc_msgSend_uncachedUNWIND __objc_msgSend_uncached, FrameWithNoSaves// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band p15 is the class to searchMethodTableLookupTailCallFunctionPointer x17END_ENTRY __objc_msgSend_uncached

其中调用了MethodTableLookup宏: 从方法列表中去查找方法
看一下它的结构:

 .macro MethodTableLookupSAVE_REGS MSGSEND// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)// receiver and selector already in x0 and x1mov	x2, x16mov	x3, #3bl	_lookUpImpOrForward// IMP in x0mov	x17, x0RESTORE_REGS MSGSEND.endmacro
总结消息转送快速查找IMP

objc_msgSend(receiver, sel, …)

  1. 检查接受者是否存在,为nil则不做任何处理;
  2. 通过receiverdeisa指针找到对应的class类对象;
  3. 找到类对象之后通过内存平移找到cache;
  4. 从cache中获取buckets;
  5. 从buckets中对比sel,查看是否有同名方法;
  6. 如果有对应的sel,就会进入到cacheHit,调用imp;
  7. 如果没有对应的sel,进入objc_msgSend_uncached,然后到lookUpImpOrForward(慢速查找)。

方法缓存:

如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率。

慢速查找

 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())) {...省略部分for (unsigned attempts = unreasonableClassCount();;) {if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHESimp = 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;}}// 未找到实现。请尝试一次方法解析器。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_CACHESwhile (cls->cache.isConstantOptimizedCache(/* strict */true)) {cls = cls->cache.preoptFallbackClass();}
#endiflog_and_fill_cache(cls, imp, sel, inst, curClass);}done_unlock:runtimeLock.unlock();if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {return nil;}return imp;
}
  1. 检查类是否被初始化,是否是个已知的关系,确定继承关系的准备工作。
     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的父类赋值给curClassif (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;}}

进入循环逻辑:

  • 从本类的methodList查找IMP(查找的方式是getMethodNoSuper_nolock);
  • 从本类的父类的的cache中查找(cache_getImp);
  • 从本类的父类demethodList查找IMP…继承链遍历…(父类->…->根父类);
  • 若上面任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache中;
  • 直到查找到nil,指定imp为消息转发,跳出循环。

跳出循环后的逻辑:

 done:if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下while (cls->cache.isConstantOptimizedCache(/* strict */true)) {cls = cls->cache.preoptFallbackClass();}
#endiflog_and_fill_cache(cls, imp, sel, inst, curClass);}done_unlock:runtimeLock.unlock();if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {return nil;}return imp;

如果找到了imp,就会把imp缓存到本类cache里(log_and_fill_cache):(注意这里不管是本类还是本类的父类找到了imp,都会缓存到本类中去)。

 static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGINGif (slowpath(objcMsgLogEnabled && implementer)) {bool cacheIt = logMessageSend(implementer->isMetaClass(), cls->nameForLogging(),implementer->nameForLogging(), sel);if (!cacheIt) return;}
#endifcls->cache.insert(sel, imp, receiver); // 插入缓存
}
  1. getMethodNoSuper_nolock查找方式
 tatic 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;
}

search_method_list_inline里找到了method_t就会返回出去了(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 listif (auto *m = findMethodInUnsortedMethodList(sel, mlist))return m;}#if DEBUG// sanity-check negative resultsif (mlist->isFixedUp()) {for (auto& meth : *mlist) {if (meth.name() == sel) {_objc_fatal("linear search worked when binary search did not");}}}
#endifreturn nil;
}

这里就是使用findMethodInSortedMethodListfindMethodInUnsortedMethodList通过sel找到method_t的。这两个函数的区别就是:
前者是排好序的,后者是未排好序的;前者方法中的查询方式是二分查找,后者则是普通查找。

总结消息传递慢速查找IMP

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

  1. 从本类的 method list (二分查找/遍历查找)查找imp
  2. 从本类的父类的cache查找imp(汇编)
  3. 从本类的父类的method list (二分查找/遍历查找)查找imp
    …继承链遍历…(父类->…->根父类)里找cache和method list的imp
  4. 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache,并返回imp
  5. 直到查找到nil,指定imp为消息转发,跳出循环,执行动态方法解析resolveMethod_locked

2. 消息转发

当一个对象无法接收某一消息时,就会启动所谓“消息转发(message forwarding)”机制。通过消息转发机制,我们可以告诉对象如何处理未知的消息。

消息转发机制大致可以分为三个步骤:

  • 动态方法解析
  • 备援接受者
  • 完整消息转发

下图为消息转发过程的示意图:
在这里插入图片描述

动态决议

// No implementation found. Try method resolver once.
//未找到实现。尝试一次方法解析器
if (slowpath(behavior & LOOKUP_RESOLVER)) {behavior ^= LOOKUP_RESOLVER;return resolveMethod_locked(inst, sel, cls, behavior);
}

通过之前的源码可以发现,如果没有找到方法则尝试调用resolveMethod_locked动态解析,只会执行一次:

 /***********************************************************************
* 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)
{lockdebug::assert_locked(&runtimeLock);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);if (!lookUpImpOrNilTryCache(inst, sel, cls)) {resolveInstanceMethod(inst, sel, cls);}}// chances are that calling the resolver have populated the cache// so attempt using itreturn lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

主要用的的方法如下:

 // 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//其中参数sel为未处理的方法

上述代码大致流程:

  • 先判断进行解析的是不是元类
  • 如果不是元类,则调用则调用resolveInstanceMethod进行对象方法动态解析
  • 如果是元类,则调用resolveClassMethod进行类方法动态解析,完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析。

而这两个方法resolveInstanceMethodresolveClassMethod则称为方法的动态决议。
执行完上述代码后返回lookUpImpOrForwardTryCache:

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

这个方法调用的是_lookUpImpTryCache方法:

 ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{lockdebug::assert_unlocked(&runtimeLock);if (slowpath(!cls->isInitialized())) {// see comment in lookUpImpOrForwardreturn lookUpImpOrForward(inst, sel, cls, behavior);}IMP imp = cache_getImp(cls, sel);if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHESif (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);}
#endifif (slowpath(imp == NULL)) {return lookUpImpOrForward(inst, sel, cls, behavior);}done:if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {return nil;}return imp;
}

进入_lookUpImpTryCache源码,可以看到这里有cache_getImp;也就是说在进行一次动态决议之后,还会通过cache_getImpcache里找一遍方法的sel

#endif
if (slowpath(imp == NULL)) {return lookUpImpOrForward(inst, sel, cls, behavior);
}

如果还是没找到(imp == NULL)?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward方法,这里behavior传的值会发生变化。

第二次进入lookUpImpOrForward方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时:

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

根据变化后的behavior值和LOOKUP_RESOLVER值之间的关系导致该if语句内部只能进入第一次,因此这个判断相当于单例。解释了为什么开头说的该动态解析resolveMethod_locked为什么只执行一次。

动态解析添加方法

在动态决议阶段可以为类添加方法,以保证程序正常运行

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

我们来看一个例子:

// Dog.h
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface Dog : NSObject
- (void)print;
@endNS_ASSUME_NONNULL_END
//Dog.m
#import "Dog.h"
#import <objc/runtime.h>
#import "NiuBiDog.h"
@implementation Dog@end

可以看到print方法并未实现,所以在主函数中调用程序一定会崩溃,
然后我们将代码修改为下面这样:
在.m文件中增加这个方法:

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

程序依然会崩溃,我们看看输出结果:
在这里插入图片描述
是因为找不到imp而崩溃,那么我们可以在这个方法里通过runtime的class_addMethod,给sel动态的生成imp。其中第四个参数是返回值类型,用void用字符串描述:“v@:”

 + (BOOL)resolveInstanceMethod:(SEL)sel {NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));if (sel == @selector(print)) {IMP imp = class_getMethodImplementation(self, @selector(add));class_addMethod(self, sel, imp, "v@");return YES;}return [super resolveInstanceMethod:sel];
}- (void)add {NSLog(@"12345");
}

在这里插入图片描述

快速转发

当cache没有找到imp,类的继承链里的方法列表都没有找到imp,并且resolveInstanceMethod / resolveClassMethod 返回NO就会进入消息转发。也就是所以如果本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。

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

imp == (IMP)_objc_msgForward_impcache进入消息转发机制。
查看一下这个方法:
竟然是汇编实现的这就又印证了汇编速度更快的结论:

 	STATIC_ENTRY __objc_msgForward_impcache// No stret specialization.b	__objc_msgForwardEND_ENTRY __objc_msgForward_impcacheENTRY __objc_msgForwardadrp	x17, __objc_forward_handler@PAGEldr	p17, [x17, __objc_forward_handler@PAGEOFF]TailCallFunctionPointer x17END_ENTRY __objc_msgForward
  • Dog类中定义print方法但是不实现,利用forwardingTargetForSelector:(SEL)aSelector 方法进行消息快速转发。
  • NiuBiDog类中定义print方法且实现:
//NiuBiDog.h
#import "Dog.h"NS_ASSUME_NONNULL_BEGIN@interface NiuBiDog : Dog
- (void)print;
@endNS_ASSUME_NONNULL_END//NiuBiDog.m#import "NiuBiDog.h"@implementation NiuBiDog- (void)print {NSLog(@"\n%s", __func__);
}@end//Dog.m
#import "Dog.h"
#import <objc/runtime.h>
#import "NiuBiDog.h"
@implementation Dog
// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(print)) {return  [NiuBiDog new];}return [super forwardingTargetForSelector:aSelector];
}
@end

在这里插入图片描述
转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。

慢速转发

如果消息的快速转发也没有找到方法;后面还有个methodSignatureForSelector方法,作用是方法有效性签名。
将刚才使用快速转发forwardingTargetForSelector方法注释后,添加上methodSignatureForSelector方法后,这个方法需要搭配forwardInvocation

forwardInvocation方法提供了一个入参,类型是NSInvocation;它提供了target和selector用于指定目标里查找方法实现。

 // 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {NSLog(@"%s  aSelector = %@", __func__, NSStringFromSelector(aSelector));return [NSMethodSignature signatureWithObjCTypes:"v@"];
}- (void)forwardInvocation:(NSInvocation *)anInvocation {NSLog(@" %s, 甘,文,崔", __func__);
}

在这里插入图片描述

总结

防止系统崩溃的三个救命稻草:动态解析快速转发慢速转发
OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程。

动态决议

// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//其中参数sel为未处理的方法

消息转发

消息快速转发:

 - (id)forwardingTargetForSelector:(SEL)aSelector;

消息慢速转发:

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

消息的三次拯救

  • 动态方法解析
  • 备援接收者
  • 完整消息转发
    在这里插入图片描述

相关文章:

【iOS】—— 消息传递和消息转发

【iOS】—— 消息传递和消息转发 1. 消息传递SEL选择子IMP快速查找汇编代码查找过程总结消息转送快速查找IMP 慢速查找总结消息传递慢速查找IMP 2. 消息转发动态决议动态解析添加方法 快速转发慢速转发 总结动态决议消息转发消息的三次拯救 1. 消息传递 在iOS中&#xff0c;消…...

【Node.js】初识 Node.js

Node.js 概念 Node.js 是一个开源与跨平台的 JavaScript运行时环境 &#xff0c;在浏览器外运行 V8 JavaScript 引擎(Google Chrome的内核)&#xff0c;利用事件驱动、非阻塞和异步输入输出 等技术提高性能。 可以理解为 Node.js就是一个服务器端的、非阻塞式 l/O 的、事件驱…...

AWS backup服务和 RDS snapshot的关系

首先&#xff0c;其实RDS的snapshot&#xff0c;自动备份和手动备份&#xff0c;就是调用的AWS backup服务&#xff0c;只不过是通过RDS控制台&#xff0c;API等等进行控制和管理的。 1. AWS backup 服务对于RDS的备份来说包括两部分&#xff1a; --连续备份&#xff08;需要…...

PDF转Word怎么快速转换?格式转换技巧分享

PDF文件和Word文档是我们日常工作中不可或缺的文件格式&#xff0c;同时文件之间的格式转换也十分常见。不同的格式有着不同的优点&#xff0c;将PDF文件改为Word文档后&#xff0c;在编辑或修改文件内容时更为方便。 下面小编就来给大家介绍几种常用的PDF转Word的转换方法&am…...

浅谈:网络协议及网络连接

事情的起因 怪有意思的。&#xff08;纯纯唠嗑&#xff0c;不感兴趣的可以跳过&#xff09; 我们初中&#xff0c;在学期的最后一天换教室&#xff0c;由于我们是十三班&#xff0c;是年级里面的一个“例外”。因为我们其他年级都是12个和10个班级&#xff0c;就我们一个奇数…...

websocket-react使用

问题 在一个应用中&#xff0c;如果需要在不同的组件之间共享同一个WebSocket连接&#xff0c;可以采用多种方法来实现。 比如&#xff1a;单例模式、全局变量、react context React上下文&#xff08;React Context&#xff09; 如果你使用的是React&#xff0c;可以使用Re…...

【总结】nginx源码编译安装报错./configure: error: SSL modules require the OpenSSL library.

问题现象 源码编译安装nginx时&#xff0c;执行./configure …… --with-http_ssl_module 命令安装https模块&#xff0c;需要用到openssl&#xff0c;由于机器缺少openssl库&#xff0c;报如下错误。 …… checking for openat(), fstatat() ... found checking for getaddr…...

昇思25天学习打卡营第15天|两个分类实验

打卡 目录 打卡 实验1&#xff1a;K近邻算法实现红酒聚类 数据准备 模型构建--计算距离 计算演示 模型预测 实验2&#xff1a;基于MobileNetv2的垃圾分类 任务说明 数据集 参数配置&#xff08;训练/验证/推理&#xff09; 数据预处理 MobileNetV2模型搭建 Mobile…...

实践:Redis6.0配置文件解读

详细解读redis配置文件 https://raw.githubusercontent.com/redis/redis/6.2/redis.conf Units 配置数据单位换算关系配置大小单位&#xff1a;当需要内存大小时&#xff0c;可以指定。开头定义了一些基本的度量单位&#xff0c;只支持bytes&#xff0c;不支持bit&#xff0…...

【Go系列】Go语言的网络服务

承上启下 我们既然知道了Go语言的语法&#xff0c;也了解到了Go语言如何协同工作机制。那么对于这样一款天生支持高并发的语言&#xff0c;它的用武之地自然而然的就是网络服务了。我们今天学学如何使用网络服务。 开始学习 Go语言使用网络服务 在Go语言中&#xff0c;使用网…...

CS110L(Rust)

1.Rust 语法总结 数值类型 有符号整数: i8, i16, i32, i64无符号整数: u8, u16, u32, u64 变量声明 声明变量: let i 0; // 类型推断let n: i32 1; // 显式类型声明 可变变量: let mut n 0; n n 1; 字符串 注意&#xff0c;let s: str "Hello world";…...

免费恢复软件有哪些?电脑免费使用的 5 大数据恢复软件

您是否在发现需要的文件时不小心删除了回收站中的文件&#xff1f;您一定对误操作感到后悔。文件永远消失了吗&#xff1f;还有机会找回它们吗&#xff1f;当然有&#xff01;您可以查看这篇文章&#xff0c;挑选 5 款功能强大的免费数据恢复软件&#xff0c;用于 Windows 和 M…...

Flink History Server配置

目录 问题复现 History Server配置 HADOOP_CLASSPATH配置 History Server配置 问题修复 启动flink集群 启动Histroty Server 问题复现 在bigdata111上执行如下命令开启socket&#xff1a; nc -lk 9999 如图&#xff1a; 在bigdata111上执行如下命令运行flink应用程序 …...

ASPICE过程改进原则:确保汽车软件开发的卓越性能

"在汽车行业中&#xff0c;软件已经成为驱动创新和增强产品功能的核心要素。然而&#xff0c;随着软件复杂性的增加&#xff0c;确保软件质量、可靠性和性能成为了一项严峻的挑战。ASPICE标准的引入&#xff0c;为汽车软件开发提供了一套全面的过程改进框架&#xff0c;以…...

HDU1005——Number Sequence,HDU1006——Tick and Tick,HDU1007——Quoit Design

目录 HDU1005——Number Sequence 题目描述 超时代码 代码思路 正确代码 代码思路 HDU1006——Tick and Tick 题目描述 运行代码 代码思路 HDU1007——Quoit Design 题目描述 运行代码 代码思路 HDU1005——Number Sequence 题目描述 Problem - 1005 超时代码…...

uniapp form表单校验

公司的一个老项目&#xff0c;又要重新上架&#xff0c;uniapp一套代码&#xff0c;打包生成iOS端发布到App Store&#xff0c;安卓端发布到腾讯应用宝、OPPO、小米、华为、vivo&#xff0c;安卓各大应用市场上架要求不一样&#xff0c;可真麻烦啊 光一个表单校验&#xff0c;…...

构建RSS订阅机器人:观察者模式的实践与创新

在信息爆炸的时代&#xff0c;如何高效地获取和处理信息成为了一个重要的问题。RSS订阅机器人作为一种自动化工具&#xff0c;能够帮助我们从海量信息中筛选出我们感兴趣的内容。 一、RSS 是什么&#xff1f;观察者模式又是什么&#xff1f; RSS订阅机器人是一种能够自动订阅…...

芯片基础 | `wire`类型引发的学习

在Verilog中&#xff0c;wire类型是一种用于连接模块内部或模块之间的信号的数据类型。wire类型用于表示硬件中的物理连线&#xff0c;它可以传输任何类型的值&#xff08;如0、1、高阻态z等&#xff09;&#xff0c;但它在任何给定的时间点上只能有一个确定的值。 wire类型通…...

如何在AWS上构建Apache DolphinScheduler

引言 随着云计算技术的发展&#xff0c;Amazon Web Services (AWS) 作为一个开放的平台&#xff0c;一直在帮助开发者更好的在云上构建和使用开源软件&#xff0c;同时也与开源社区紧密合作&#xff0c;推动开源项目的发展。 本文主要探讨2024年值得关注的一些开源软件及其在…...

Quartus II 13.1添加新的FPGA器件库

最近需要用到Altera的一款MAX II 系列EPM240的FPGA芯片&#xff0c;所以需要给我的Quartus II 13.1添加新的器件库&#xff0c;在此记录一下过程。 1 下载所需的期间库 进入Inter官网&#xff0c;&#xff08;Altera已经被Inter收购&#xff09;https://www.intel.cn/content…...

【html】html的基础知识(面试重点)

一、如何理解HTML语义化 1、思考 A、在没有任何样式的前提下&#xff0c;将代码在浏览器打开&#xff0c;也能够结构清晰的展示出来。标题是标题、段落是段落、列表是列表。 B、便于搜索引擎优化。 2、参考答案 A、让人更容易读懂&#xff08;增加代码可读性&#xff09;。 B、…...

Java 网络编程(TCP编程 和 UDP编程)

1. Java 网络编程&#xff08;TCP编程 和 UDP编程&#xff09; 文章目录 1. Java 网络编程&#xff08;TCP编程 和 UDP编程&#xff09;2. 网络编程的概念3. IP 地址3.1 IP地址相关的&#xff1a;域名与DNS 4. 端口号&#xff08;port&#xff09;5. 通信协议5.1 通信协议相关的…...

STM32 | 看门狗+RTC源码解析

点击上方"蓝字"关注我们 作业 1、使用基本定时7,完成一个定时喂狗的程序 01、上节回顾 STM32 | 独立看门狗+RTC时间(第八天)02、定时器头文件 #ifndef __TIM_H#define __TIM_H​#include "stm32f4xx.h"​void Tim3_Init(void);void Tim7_Init(void);​…...

filebeat,kafka,clickhouse,ClickVisual搭建轻量级日志平台

springboot集成链路追踪 springboot版本 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version><relativePath/> <!-- lookup parent from…...

Django实战项目之进销存数据分析报表——第一天:Anaconda 环境搭建

引言 Anaconda是一个流行的Python和R语言的发行版&#xff0c;它包含了大量预安装的数据科学、机器学习库和科学计算工具。使用Anaconda可以轻松地创建隔离的环境&#xff0c;每个环境都可以有自己的一套库和Python版本&#xff0c;非常适合多项目开发。本文将指导你如何安装A…...

Linux部署Prometheus+Grafana

【Linux】PrometheusGrafana 一、Prometheus&#xff08;普罗米修斯&#xff09;1、Prometheus简述2、Prometheus特点3、Prometheus生态组件4、Prometheus工作原理 二、部署Prometheus1、系统架构2、部署Prometheus3、修改配置文件4、配置系统启动文件 三、部署 Node Exporter …...

【视频讲解】神经网络、Lasso回归、线性回归、随机森林、ARIMA股票价格时间序列预测|附代码数据

全文链接&#xff1a;https://tecdat.cn/?p37019 分析师&#xff1a;Haopeng Li 随着我国股票市场规模的不断扩大、制度的不断完善&#xff0c;它在金融市场中也成为了越来越不可或缺的一部分。 【视频讲解】神经网络、Lasso回归、线性回归、随机森林、ARIMA股票价格时间序列…...

低代码前端框架Amis全面教程

什么是Amis&#xff1f; 1.1 Amis的基本概念 Amis是一个基于JSON配置的前端低代码框架&#xff0c;由百度开源。它允许开发者通过简单的JSON配置文件来生成复杂的后台管理页面&#xff0c;从而大大减少了前端开发的工作量。Amis的核心理念是通过配置而非编码来实现页面的构建…...

Windows 如何安装和卸载 OneDrive?具体方法总结

卸载 OneDrive 有人想问 OneDrive 可以卸载吗&#xff1f;如果你不使用当然可以卸载&#xff0c;下面是安装和卸载 OneDrive 中的卸载应用具体操作步骤&#xff1a; 卸载 OneDrive 我们可以从设置面板中的应用选项进行卸载&#xff0c;打开设置面板之后选择应用&#xff0c;然…...

c# .net core中间件,生命周期

某些模块和处理程序具有存储在 Web.config 中的配置选项。但是在 ASP.NET Core 中&#xff0c;使用新配置模型取代了 Web.config。 HTTP 模块和处理程序如何工作 官网地址&#xff1a; 将 HTTP 处理程序和模块迁移到 ASP.NET Core 中间件 | Microsoft Learn 处理程序是&#xf…...

哪个建站平台较好/指数函数图像

有序表的三种经典查找算法&#xff1a;折半查找&#xff0c;插值查找&#xff0c;斐波拉契查找 0x01.折半查找 算法过于简单&#xff0c;代码如下&#xff1a; //a为有序数组&#xff0c;n为a数组的末位&#xff0c;key为要查找的值,返回下标 int Binary_Search(int* a, int…...

南宁seo平台标准/seo综合查询中的具体内容有哪些

1.Jmeter 基本介绍 简单的说&#xff0c;就是Jmeter能做 功能测试 和 性能测试 。它能够对HTTP和FTP服务器进行压力和性能测试&#xff0c; 也可以对任何数据库进行同样的测试&#xff0c;还能以多种形式展现测试结果。 比如&#xff0c;我一 开始使用这个工具就是为了接口测试…...

网站开发需要看相关书籍/搜狗搜索旧版本

前言因为工作需要可能要用到JNI开发&#xff0c;本篇文章就分享一下我在这方面的实践&#xff0c;以前我们使用Eclipse做NDK开发&#xff0c;非常麻烦&#xff0c;需要配cygwin的编译环境&#xff0c;后面NDK功能完善才逐渐简单点&#xff0c;Eclipse的如何通过NDK生成so库就不…...

建设政协网站的意义/seo综合查询网站源码

今天又翻看了鑫的博客&#xff0c;收获不少&#xff0c;记录收藏一下 &#xff11;.absolute的重新认识&#xff0c;absolute绝对定位的非绝对定位用法  &#xff12;.尽量不使用浮动面布局&#xff0c;使用display:inline-block 基于display:inline-block的列表布局 &…...

三合一网站开发架构/十大搜索引擎排名

WiFi发展历程 IEEE 802.11ax 标准现在称为 Wi-Fi 6 或第 6 代 Wi-Fi&#xff0c;并在 2.4 GHz 和 5 GHz 频段运行。 Wi-Fi 6E 在 6 GHz 频段运行。 WiFi 6E信道分布...

wordpress悬浮搜索/网络营销的优势和劣势

Python文本处理尝试 最近打算看CSAPP&#xff0c;GitHub上看到有英语字幕ass源文件&#xff0c;想把字幕提取出来提高学习效率&#xff0c;先把ass文件转成txt文件&#xff0c;发现是这样&#x1f447; 都在Dialogue的后面&#xff0c;打算尝试提取一下 不太熟练&#xff0c;下…...