【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;

消息的三次拯救

  • 動態方法解析
  • 備援接收者
  • 完整消息轉發
    在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/46454.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/46454.shtml
英文地址,請注明出處:http://en.pswp.cn/web/46454.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

一、單例模式

文章目錄 1 基本介紹2 實現方式2.1 餓漢式2.1.1 代碼2.1.2 特性 2.2 懶漢式 ( 線程不安全 )2.2.1 代碼2.2.2 特性 2.3 懶漢式 ( 線程安全 )2.3.1 代碼2.3.2 特性 2.4 雙重檢查2.4.1 代碼2.4.2 特性 2.5 靜態內部類2.5.1 代碼2.5.2 特性 2.6 枚舉2.6.1 代碼2.6.2 特性 3 實現的要…

【樂吾樂2D可視化組態編輯器】快捷鍵

快捷鍵 樂吾樂2D可視化組態編輯器demo&#xff1a;https://2d.le5le.com/ 快捷鍵描述空格 鼠標拖拽移動畫布鼠標右鍵拖拽移動畫布Ctrl 滾輪縮放畫布Ctrl 點擊 Pen多選Ctrl A全選Ctrl C復制Ctrl X剪切Ctrl V粘貼&#xff0c;alt視圖中心粘貼&#xff0c;shift原位粘貼…

查詢優化 -- UNION 用法

union 不返回重復行&#xff08;所有字段值相同的行&#xff09; union all 返回所有行 // 每類最多統計100條 select server_id,count(1) as logs from ( SELECT server_id FROM log WHERE log.type "a" AND server_id1 limit 100 ) UNION select server_id,coun…

谷粒商城-全文檢索-ElasticSearch

1.簡介 一個分布式的開源搜索和分析引擎,可以 秒 級的從海量數據中檢索 主要功能:做數據的檢索和分析(MySQL專攻于數據的持久化存儲與管理CRUD達到百萬以上的數據MSQL就會很慢,海量數據的檢索和分析還是要用ElasticSearch) 用途:我們電商項目里的所有的檢索功能都是由Elasti…

Java中為什么不能直接創建泛型數組

在Java中&#xff0c;不能直接創建泛型數組的主要原因是類型擦除和類型安全問題。 類型擦除 Java中的泛型是通過類型擦除&#xff08;Type Erasure&#xff09;實現的&#xff0c;這意味著在編譯時&#xff0c;泛型類型會被轉換成原始類型&#xff08;如 List<T> 會被轉…

網絡安全-網絡安全及其防護措施9

41.網絡故障排除 網絡故障排除的定義和重要性 網絡故障排除是指通過系統化的方法和工具&#xff0c;識別、診斷和解決網絡中出現的問題&#xff0c;以恢復正常的網絡服務和性能。有效的故障排除可以減少停機時間&#xff0c;提升網絡的穩定性和可靠性。 故障排除的步驟 問題…

基于X86+FPGA+AI數字化醫療設備:全自動尿沉渣檢測儀

助力數字醫療發展&#xff0c;信邁可提供全自動尿沉渣檢測儀專用計算機 隨著信息技術的不斷進步&#xff0c;醫療也進入了一個全新的數字化時代。首先是醫療設備的數字化&#xff0c;大大豐富了醫療信息的內涵和容量&#xff0c;具有廣闊的市場發展前景。 數字化醫療設備&…

使用Redis的SETNX命令實現分布式鎖

什么是分布式鎖 分布式鎖是一種用于在分布式系統中控制多個節點對共享資源進行訪問的機制。在分布式系統中&#xff0c;由于多個節點可能同時訪問和修改同一個資源&#xff0c;因此需要一種方法來確保在任意時刻只有一個節點能夠對資源進行操作&#xff0c;以避免數據不一致或…

白騎士的C++教學高級篇 3.1 文件操作

系列目錄 上一篇&#xff1a;白騎士的C教學進階篇 2.4 標準模板庫&#xff08;STL&#xff09; 文件操作是C編程中的一個重要部分&#xff0c;允許程序與外部存儲設備進行交互&#xff0c;從而實現數據的持久化存儲和讀取。C標準庫提供了豐富的文件操作功能&#xff0c;包括文…

嵌入式香橙派人工智能AI開發板詳細操作與遠程聊天實現

大家好&#xff0c;今天給大分享一個OrangePi AIpro&#xff08;20T&#xff09;采用昇騰作為主控芯片的開發板&#xff0c;開箱以及對應功能的詳細實現。 第一&#xff1a;板子基本介紹 接通電源給對應的開發板上電&#xff0c;觀察其中的現象&#xff0c;如下&#xff1a; 注…

基于HAL庫的stm32的OLED顯示屏顯示(IIC)

OLED OLED&#xff0c;即有機發光二極管( Organic Light Emitting Diode )。OLED由于同時具備自發光&#xff0c;不需背光源、對比度高、厚度薄、視角廣、反應速度快、可用于撓曲性面板、使用溫度范圍廣、構造及制程較簡單等優異之特性&#xff0c;被認為是下一代的平面顯示器…

龍國專利局瑞數6

聲明(lianxi a15018601872) 本文章中所有內容僅供學習交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包內容、敏感網址、數據接口等均已做脫敏處理&#xff0c;嚴禁用于商業用途和非法用途&#xff0c;否則由此產生的一切后果均與作者無關&#xff01; 前言(lianxi a…

富文本中提取信息并去除其中的HTML或XML標簽

要從富文本中提取信息并去除其中的HTML或XML標簽&#xff0c;可以使用不同的編程語言和庫。以下是一些流行語言中的示例方法&#xff1a; 1. Python&#xff08;使用BeautifulSoup&#xff09; BeautifulSoup是一個強大的Python庫&#xff0c;用于從HTML或XML文件中提取數據。…

巨魔商店(TrollStore)介紹與使用指南

iOS巨魔商店&#xff08;TrollStore&#xff09;介紹與使用指南 引言 在iOS系統中&#xff0c;App Store是官方唯一的應用下載渠道&#xff0c;但這也限制了用戶獲取非官方或破解版應用的可能性。然而&#xff0c;巨魔商店&#xff08;TrollStore&#xff09;的出現打破了這一…

配置和保護SSH

使用SSH訪問遠程命令行 描述Secure Shell SSH&#xff08;Secure Shell&#xff09; 是一種網絡協議&#xff0c;用于在不安全的網絡上安全地進行系統管理和數據傳輸。它最初由 Tatu Ylnen 于1995年設計&#xff0c;并成為保護網絡服務免受攻擊的標準。SSH提供了多種功能&…

開始構建我們自己的大語言模型:數據處理部分

關注本專欄&#xff08;NLP簡論&#xff1a;手搓大語言模型實踐&#xff09; 繼續學習從頭編寫、訓練自己的大語言模型。 接上集&#xff0c;本章我們將深入說一下大語言模型數據處理部分的細節&#xff0c;并直接提供本部分的完整代碼。 【配套資源】 暫時的詞匯表&#xff1…

GNN論文粗讀

論文 文章目錄 論文基于異構圖的GNN論文GNN領域論文環境領域GNN論文 隨緣更新 基于異構圖的GNN論文 Distance Information Improves Heterogeneous Graph Neural Networks:DOI: 10.1109/TKDE.2023.3300879 轉導和歸納任務&#xff0c;創新點&#xff1a;異構距離編碼HDE提高GN…

關于Vue中涉及到大量數據的級聯菜單樹狀結構的數據多選勾選頁面卡頓卡死問題

項目場景&#xff1a;如題 提示&#xff1a;有個需求&#xff0c;級聯菜單樹狀結構的數據高達3萬多條&#xff0c;多選&#xff0c;只需要最后一層級value 原因分析&#xff1a;頁面一下子渲染大量數據會導致瀏覽器內存暴漲100%&#xff0c;導致頁面卡死&#xff0c;而且eleme…

常見Linux目錄和配置文件

目錄 /boot/&#xff1a;開機配置文件&#xff0c;也是存放核心vmlinuz的地方 /bin/&#xff1a;系統可執行文件目錄&#xff0c;CentOS7后合并到/usr/bin中&#xff0c;并鏈接過去 /sbin/&#xff1a;系統管理員常用指令存放目錄&#xff0c;例如開關機、磁盤分區等指令&am…

基于SpringBoot+Vue的廣場舞團系統(帶1w+文檔)

基于SpringBootVue的廣場舞團系統(帶1w文檔) 基于SpringBootVue的廣場舞團系統(帶1w文檔) 廣場舞團&#xff0c;為用戶隨時隨地查看廣場舞團信息提供了便捷的方法&#xff0c;更重要的是大大的簡化了管理員管理廣場舞團信息的方式方法&#xff0c;更提供了其他想要了解廣場舞團…