objc_msgSend分析-快速查找

objc_msgSend快速查找

閱讀此文須要對於objc_object、objc_class以及結構體內部cache_t有必定的瞭解。c++

環境:xcode 11.5算法

源碼:objc4-781數組

當咱們對oc代碼進行clang處理轉換爲c++代碼以後,咱們會發現,oc的方法調用會被進行以下轉換:xcode

源碼:
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];
轉換以後:
LGTeacher *teacher = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("sayHello"));
複製代碼

最終都會調用消息發送objc_msgSend方法,定義以下:緩存

objc_msgSend(id receiver, SEL op, ...)
第一個參數id receiver爲消息的接收者
第二個參數SEL op爲消息的名稱SEL
...爲可變參數
複製代碼

objc_msgSend是全部OC方法調用的核心,調用效率是至關的高,所以處於性能考慮,這個函數的內部代碼是用匯編來實現。在arm64下的具體核心實現以下所示:markdown

以以下代碼爲例:
// objc_msgSend(id receiver, SEL op, ...)

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
    //判斷receiver是否爲空
	cmp	p0, #0			// nil check and tagged pointer check
    //處理異常邏輯
#if SUPPORT_TAGGED_POINTERS
    //le  less than or equal
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
    // 讀取x0寄存器即receiver的首地址所指向的內容存入p13,receiver是objc_object結構體指針,所以首地址對應的內存區域存放的是isa
	ldr	p13, [x0]    	// p13 = isa
    // 從isa中獲取Class,存入p16
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
    // 查找緩存,調用緩存或者進行快速查找objc_msgSend_uncached
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend
複製代碼

objc_msgSend的主要流程爲:less

  1. 從當前的消息接收者receiver中查找到receiver對應的isa指針進而找到對應的類。
  2. 在類中查找對應的方法並執行

接下來看一下具體的幾個方法。函數

GetClassFromIsa_p16

該方法主要用來從isa指針來獲得對應的類。oop

.macro GetClassFromIsa_p16 /* src */
...
#elif __LP64__
	//只關注arm64
    // $0爲上述流程中傳入的參數p13, 
    // p16 = isa & #ISA_MASK, ISA_MASK爲isa.h中聲明的掩碼,經過這一步操做,能夠獲得isa_t結構體中對應的cls
	and	p16, $0, #ISA_MASK
...
複製代碼

CacheLookup

.macro CacheLookup
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart$1 label we may have loaded
	//   an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd$1,
	//   then our PC will be reset to LLookupRecover$1 which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//
LLookupStart$1:
//#define CACHE            (2 * __SIZEOF_POINTER__)

	// p1 = SEL, p16 = isa
    // 讀取x16+16個字節長度爲地址的內容,存入p11中,根據objc_class的結構,cache_t和isa的偏移量就是16個字節,分別是isa8個字節以及superClassb8個字節
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
    
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // 只關注arm64
    // 0x0000ffffffffffff爲arm64下的buckets掩碼, 按位與以後獲得buckets指針
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
    // p1爲msgSend的第二個參數_cmd
    // p11=cache_t右移48位獲得cache_t中的mask
    // _cmd & mask和方法緩存中的哈希算法一致,以此來進行快速緩存方法查找
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11				// p11 = mask = 0xffff >> p11
	and	p12, p1, p11				// x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // PTRSHIFT 3
    // p12即hash值左移4位至關於p12*16,獲得對應的bucket指針相對於bucket數組的偏移量,進而獲得當前hash值對應的bucket指針放入p12中
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    // 讀取x12內容爲地址的值依次放入p17和p9中,arm64中bucket_t結構體佈局爲{imp,sel}

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
    // 比較p9=sel和p1=cmd
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
    // 不相等,向前跳轉至2
	b.ne	2f			//     scan more
    // 相等,執行命中的指令
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
    // 根據sel的值判斷是否查找結束
	CheckMiss $0			// miss if bucket->sel == 0
    // 若是sel不爲0,比較當前命中的bucket是否和buckets數組的第一個bucket相等
	cmp	p12, p10		// wrap if bucket == buckets
    // 相等,跳轉至3
	b.eq	3f
    // 不相等,以x12即當前bucket地址往前移動一個bucket大小爲地址讀取其中的值,並賦值爲p17和p9,同時x12=x12-BUCKET_SIZE
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
    // 返回1進行循環
	b	1b			// loop

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // 接上步驟,若是p12和buckets數組第一個相等,須要移動到buckets數組的最後一位
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p12, p12, p11, LSL #(1+PTRSHIFT)
					// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.
    // 賦值繼續進行比較,和上述步驟類似
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
    // 若是從buckets數組最後一個開始往前尋找又找到了buckets數組第一個bucket,表明查找結束,並無找到當前的sel,執行JumpMiss操做。
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
	JumpMiss $0

.endmacro
複製代碼

這一步主要操做是在類objc_class中的緩存cache_t中快速查找是否有緩存。主要步驟以下:佈局

  1. 經過_cmd&mask獲得hash
  2. 根據hash值獲得對應的bucket->sel與_cmd做對比
  3. 執行CheckMiss,若是bucket->sel==0表明_cmd對應的方法並無緩存過,或者在緩存擴容的時候被清空了。
  4. 若是bucket->sel!=_cmd,則往前一個bucket繼續進行對比,若是到達buckets數組第一個則從數組的最後一個bucket開始對比。
  5. 若是從最後一個bucket開始往前查找又回到了第一個,則表明出現了問題,緩存已經滿了,執行JumpMiss
  6. 若是bucket->sel==_cmd,命中緩存,執行CacheHit

CacheHit

// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
    // NORMAL狀況下回執行調用imp的流程
	TailCallCachedImp x17, x12, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x12, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x12, x1, x16	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro


複製代碼

CheckMiss

.macro CheckMiss
	// miss if bucket->sel == 0
    // cbz p9 判斷p9是否爲0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
    // NORMAL狀況下回執行調用objc_msgSend_uncached的流程
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製代碼

JumpMiss

.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
    // NORMAL狀況下回執行調用objc_msgSend_uncached的流程
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製代碼

其中CheckMissJumpMiss都是快速查找方法沒有找到的狀況。

  • CheckMiss是找到了一個空的位置,由於查找的算法和緩存的算法是一致的,意味着若是方法被緩存過,必然是放在當前的空位置。既然當前位置爲空,說明該方法並無被緩存過。那麼就要執行消息的慢速查找了。
  • JumpMiss表明整個緩存列表都找過了尚未找到_cmd對應的方法,這種狀況我有些疑問,cache_t是會擴容的,是否是意味着在正常的狀況下必定會有空位,那爲何還能遇到列表滿的狀況?有大佬的話能夠解答一下。

以上是objc_msgSend發送消息時在彙編層面的快速查找流程,進入__objc_msgLookup_uncached就屬於慢速查找了,後面再議。

快速查找主要流程圖以下:

相關文章
相關標籤/搜索