論objc_msgSend消息機制以前傳

一.探索前需知

1.1 runtime 的概念api

iOS語言Objective-C是一門動態語言,其在runtime(運行時)會作不少事情,好比:消息的查找、消息的轉發、動態屬性的添加等等,可是runtime究竟是什麼呢?runtime就是由C、C++、 彙編爲OC提供運行時功能開發出來的一套api.xcode

1.2 runtime的使用方式 緩存

runtime的使用方式主要有三種 : ObjectIve-C調用 @selector()、NSObject的方法 NSSelectorFromString()、sel_registerName. 函數apisass


二.objc_msgSend 的初步瞭解
bash

首先咱們在main.m 文件裏,建立了個LGPerson的對象並調用了sayNB的方法,又寫了個C語言函數run,而且調用.好的那咱們如今看下底層在編譯的時候到底作了哪些處理. 打開終端 輸入命令:clang -rewrite-objc main.m -o main.cpp(輸出個.cpp編譯後的文件)打開main.cpp文件截取其中一段以下:app

咱們看到 sayNB方法底層編譯經過objc_msgSend 底層查找函數的實現、默認帶了兩個參數 id和SEL id就是函數的接受者在這裏就是person,SEL方法編號. 而run函數底層沒有編譯成objc_msgSend 那是由於C語言中函數名就是函數指針,它經過這個函數名直接就能找到函數實現並不須要objc_msgSend這一中轉這一階段.可是objc_msgSend到底作了什麼呢?咱們先在代碼裏打個斷點、打開工程裏的Debug -> Debug Workflow - > Always Show Disassembly函數

就是打開彙編調試、咱們在方法上打個斷點:oop

發現會進來下面彙編指令:post

sayCode下有個0x100000c63 <+67>: callq *0x397(%rip) ; (void *)0x00007fff63046000: objc_msgSend. 咱們在這行上打上斷點而且按住control+xcode中step into進去發現指令會進入系統的libobjc.A.dylib的庫裏ui


接下來咱們就要在這個系統的庫中繼續研究objc_msgSend的流程了.

二.objc_msgSend 的進階

首先咱們去 www.opensource.apple.com/apsl/ 中下載目前蘋果開源的私有庫libobjc.A.dylib ,而後配置到咱們的項目裏.咱們的objc_msgSend是由彙編寫的(由彙編寫的好處是:1.能夠經過寫一個函數保留未知的函數任意的跳轉到另外個函數指針 2.彙編語言更接近機器語言 因此使用起來速度會更快)

開始在項目工程全局搜索 objc_msgSend 找到實現objc_msgSend的彙編文件


找到是這個 arm.s 的文件,那咱們進文件裏看下究竟吧.

 

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
    // person - isa - 類
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check

	// tagged
	adrp	x10, _objc_debug_taggedpointer_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
	ubfx	x11, x0, #60, #4
	ldr	x16, [x10, x11, LSL #3]
	adrp	x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
	add	x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
	cmp	x10, x16
	b.ne	LGetIsaDone

	// ext tagged
	adrp	x10, _objc_debug_taggedpointer_ext_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
	ubfx	x11, x0, #52, #8
	ldr	x16, [x10, x11, LSL #3]
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
複製代碼

代碼解析:

cmp	p0, #0 判斷是不是 nonepointerisa(是否是純淨的ISA)複製代碼
// person - isa - 類
  ldr	p13, [x0]		// p13 = isa
  GetClassFromIsa_p16 p13		// p16 = class複製代碼

還記得剛剛objc_msgsend 時會默認 傳兩個參數嗎 .idSELid 就是當前的對象 它的內存地址首地址是ISA,因此 這個對象 [xo]首地址位 就是isa .由於剛剛第一步已經判斷出不是純淨的isa,因此本身的isa內部偏移16位獲得 shiftClass也就是獲取到當前對象所在的類.

CacheLookup NORMAL		// calls imp or objc_msgSend_uncached複製代碼

接着在類的緩存裏進行查找.

(對cache_t結構不是很熟悉的小夥伴們,建議看下上篇文章juejin.im/post/5e0ca3…)

.macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	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
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
	b	1b			// loop

3:	// wrap: p12 = first bucket, w11 = mask
	add	p12, p12, w11, UXTW #(1+PTRSHIFT)
		                        // p12 = buckets + (mask << 1+PTRSHIFT)

	// 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
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0
	
.endmacro
複製代碼

 // p1 = SEL, p16 = isa

ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask

由ISA偏移16位獲得類的cache,cache裏有buckets 和 occupied|mask 分別由p10和p11進行接收。

and	w11, w11, 0xffff	// p11 = mask
#endif
and	w12, w1, w11		// x12 = _cmd & mask複製代碼

用低32位的 p11接收mask,而後 &  _cmd (也就是sel) 獲得  (mask_t)(key & mask);就是將要在緩存池buckets 進行查找的索引.

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複製代碼

緊接着就在緩存池裏進行查找若是 bucket裏的sel和_cmd匹配的話說明在緩存中已經找到,緩存命中.若是沒有找到就跳轉 2f步驟,找到則返回 , 找不到 JumpMiss .

繼續來到 __objc_msgSend_uncached -> MethodTableLookup

最後調用 bl __class_lookupMethodAndLoadCache3, 來到慢速查找流程 .至於慢速查找流程請期待下篇 《論objc_msgSend消息機制正傳之消息查找》.

四.總結

當咱們調用一個對象或者類方法的時候,系統會調用objc_msgSend進行彙編層面的查找,會什麼先用匯編?由於快!咱們詳細的探索了整個過程,彙編層面主要是對已經緩存過的IMP進行搜索。搜索的路徑是存放整個方法的類或者元類的cache_t的bucket!若是找到直接返回整個方法的實現,若是查不到,說明這個方法沒有進行緩存!那這個方法是否存在呢?請看下一篇文章,objc_msgSend在C函數_class_lookupMethodAndLoadCache3的查找和方法的動態解析!

相關文章
相關標籤/搜索