iOS 底層探索 - 方法

iOS 底層探索系列html

iOS 查漏補缺系列緩存

咱們在前面探索了對象和類的底層原理,接下來咱們要探索一下方法的本質,而在探索以前,咱們先簡單過一遍 Runtime 的知識點,若是讀者對這塊內容已經很熟悉了的話能夠直接跳過第一章。bash

PS: 因爲筆者對彙編暫時仍是摸索的階段,關於彙編源碼的部分若有錯誤,歡迎指正。markdown

1、Runtime 簡介

衆所周知,Objective-C 是一門動態語言,而承載整個 OC 動態特性的就是 Runtime。關於 Runtime 更多內容能夠直接進入官網文檔查看。app

Runtime 是以 C/C++和彙編編寫而成的,爲何不用 OC 呢,這是由於對咱們編譯器來講,OC 屬於更高級的語言,相比於 CC++ 以及彙編,執行效率更慢,而在運行時系統須要儘量快的執行效率。ide

1.1 Runtime 的前世此生

Runtime 分爲兩個版本,legacymodern,分別對標 OC 1.0OC 2.0。咱們一般只須要專一於 modern 版本便可,在 libObjc 源碼中體如今 new 後綴的文件上。函數

1.2 Runtime 三種交互方式

咱們與 Runtime 打交道有三種方式:oop

  • 直接在 OC 層進行交互:好比 @selector
  • NSObject 的方法:NSSelectorFromName
  • Runtime 的函數: sel_registerName

2、方法的本質探索

2.1 方法初探

image.png

咱們能夠看到,經過 clang 重寫以後,sayNB 在底層實際上是一個消息的發送。post

咱們把右側的發送消息的代碼簡化一下:學習

LGPerson *person = objc_msgSend((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
objc_msgSend((id)person, sel_registerName("sayNB"));
複製代碼

因而可知,真正發送消息的地方是 objc_msgSend,這個方法有兩個參數,一個是消息的接受者爲 id 類型,第二個個是方法編號 sel

做爲對比,run 方法就直接執行了,並無經過 objc_msgSend 進行消息發送:

image.png

2.2 方法發送的幾種狀況

LGStudent *s = [LGStudent new];
[s sayCode];        

objc_msgSend(s, sel_registerName("sayCode"));
複製代碼

上述代碼表示的是向對象 s 發送 sayCode 消息。


id cls = [LGStudent class];
void *pointA = &cls;
[(__bridge id)pointA sayNB];

objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));
複製代碼

上述代碼表示向 LGStudent 這個類發送 sayNB 消息。


// 向父類發消息(對象方法)
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));
複製代碼

上述代碼表示向父類發送 sayHello 消息。


//向父類發消息(類方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元類
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
複製代碼

上述代碼表示向父類的類,也就是元類發送 sayNB 消息。

咱們在 OC 中使用 objc_msgSend 的時候,要注意須要將 Enbale Strict of Checking of objc_msgSend Calls 設置爲 NO。這樣纔不會報警告。

image.png

3、探索 objc_msgSend

objc_msgSend 之因此採用彙編來實現,是由於

  • 彙編更容易能被機器識別
  • 參數未知、類型未知對於 CC++ 來講不如彙編更駕輕就熟

3.1 消息查找機制

  • 快速流程
  • 慢速流程

3.2 定位 objc_msgSend 彙編源碼

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check
複製代碼

判斷 p0 ,也就是咱們 objc_msgSend 的第一個參數 id 消息的接收者是否爲空。

ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
複製代碼

讀取 x0 而後賦值到 p13 ,這裏 p13 拿到的是 isa。爲何要拿 isa 呢,由於不管是對象方法仍是類方法,咱們都須要在類或者元類的緩存或方法列表中去查找,因此 isa 是必需的。

3.3 GetClassFromIsa_p16

經過 GetClassFromIsa_p16,將獲取到的 class 存在 p16 上面。

GetClassFromIsa_p16 源碼以下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
	// Indexed isa
	mov	p16, $0			// optimistically set dst = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa
	// isa in p16 is indexed
	adrp	x10, _objc_indexed_classes@PAGE
	add	x10, x10, _objc_indexed_classes@PAGEOFF
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:

#elif __LP64__
	// 64-bit packed isa
	and	p16, $0, #ISA_MASK

#else
	// 32-bit raw isa
	mov	p16, $0

#endif

.endmacro
複製代碼

這個方法的目的就是經過位移操做獲取 isashiftcls 而後進行位運算與操做獲得真正的類信息。

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

3.4 CacheLookup

獲取完 isa 以後,接下來就要進行 CacheLookup ,查找方法緩存,咱們再來到 CacheLookup 的源碼處:

/******************************************************************** * * CacheLookup NORMAL|GETIMP|LOOKUP * * Locate the implementation for a selector in a class method cache. * * Takes: * x1 = selector * x16 = class to be searched * * Kills: * x9,x10,x11,x12, x17 * * On exit: (found) calls or returns IMP * with x16 = class, x17 = IMP * (not found) jumps to LCacheMiss * ********************************************************************/

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

經過上述代碼可知 CacheLookup 有三種模式:NORMALGETIMPLOOKUP

ldp	p10, p11, [x16, #CACHE]
複製代碼
  • CacheLookup 須要讀取上一步拿到的類的 cache 緩存,而根據咱們前面對類結構的學習,這裏顯然進行 16 字節地址平移操做,而後把拿到的 cache_t 中的 bucketsoccupiedmask 賦值給 p10, p11
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
複製代碼
  • 這裏是將 w1w11 進行與操做,其實本質就是 _cmd & mask。這一步和咱們探索 cache_t 時遇到的
    image.png
    是一模模同樣樣的道理。目的就是拿到下標。而後通過哈希運算以後,獲得了 bucket 結構體指針,而後將這個結構體指針中的 impsel 分別存在 p17p9 中。
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			// scan more
	CacheHit $0			// call or return imp
複製代碼
  • 接着咱們將上一步獲取到的 sel 和咱們要查找的 sel(在這裏也就是所謂的 _cmd)進行比較,若是匹配了,就經過 CacheHitimp 返回;若是沒有匹配,就走下一步流程。
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
複製代碼
  • 因爲上一步的 sel 沒有匹配上,咱們須要接着進行搜索。

3.5 CheckMiss

咱們來到 CheckMiss 的源碼:

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製代碼

這裏因爲咱們是 NORMAL 模式,因此會來到 __objc_msgSend_uncached:

STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p16 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
複製代碼

__objc_msgSend_uncached 中最核心的邏輯就是 MethodTableLookup,意爲查找方法列表。

3.6 MethodTableLookup

咱們再來到 MethodTableLookup 的定義:

.macro MethodTableLookup
	
	// push frame
	SignLR
	stp	fp, lr, [sp, #-16]!
	mov	fp, sp

	// save parameter registers: x0..x8, q0..q7
	sub	sp, sp, #(10*8 + 8*16)
	stp	q0, q1, [sp, #(0*16)]
	stp	q2, q3, [sp, #(2*16)]
	stp	q4, q5, [sp, #(4*16)]
	stp	q6, q7, [sp, #(6*16)]
	stp	x0, x1, [sp, #(8*16+0*8)]
	stp	x2, x3, [sp, #(8*16+2*8)]
	stp	x4, x5, [sp, #(8*16+4*8)]
	stp	x6, x7, [sp, #(8*16+6*8)]
	str	x8,     [sp, #(8*16+8*8)]

	// receiver and selector already in x0 and x1
	mov	x2, x16
	bl	__class_lookupMethodAndLoadCache3

	// IMP in x0
	mov	x17, x0
	
	// restore registers and return
	ldp	q0, q1, [sp, #(0*16)]
	ldp	q2, q3, [sp, #(2*16)]
	ldp	q4, q5, [sp, #(4*16)]
	ldp	q6, q7, [sp, #(6*16)]
	ldp	x0, x1, [sp, #(8*16+0*8)]
	ldp	x2, x3, [sp, #(8*16+2*8)]
	ldp	x4, x5, [sp, #(8*16+4*8)]
	ldp	x6, x7, [sp, #(8*16+6*8)]
	ldr	x8,     [sp, #(8*16+8*8)]

	mov	sp, fp
	ldp	fp, lr, [sp], #16
	AuthenticateLR

.endmacro
複製代碼

咱們觀察 MethodTableLookup 內容以後會定位到 __class_lookupMethodAndLoadCache3。在 __class_lookupMethodAndLoadCache3 以前會作一些準備工做,真正的方法查找流程核心邏輯是位於 __class_lookupMethodAndLoadCache3 裏面的。 可是咱們全局搜索 __class_lookupMethodAndLoadCache3 會發現找不到,這是由於此時咱們會從彙編跳入到 C/C++。因此去掉一個下劃線就能找到:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼

4、總結

  • 方法的本質就是消息發送,消息發送是經過 objc_msgSend 以及其派生函數來實現的。
  • objc_msgSend 爲了執行效率以及 C/C++ 不能支持參數未知,類型未知的代碼,因此採用彙編來實現 objc_msgSend
  • 消息查找或者說方法查找,會優先去從類中查找緩存,找到了就返回,找不到就須要去類的方法列表中查找。
  • 由彙編過渡到 C/C++,在類的方法列表中查找失敗以後,會進行轉發。核心邏輯位於 lookUpImpOrForward

咱們下一章將會從 lookUpImpOrForward 開始探索,探索底層的方法查找的具體流程究竟是怎麼樣的,敬請期待~

相關文章
相關標籤/搜索