在上一篇文章中,我們了解了cache的寫入流程,那么是怎么進(jìn)行方法的查找呢,接下來我們在這篇以及下面的文章來進(jìn)行探討,本篇文章先對方法的快速查找進(jìn)行分析。
在分析之前,先來說一下Runtime,Runtime是給OC這門對象語言提供的運(yùn)行時(shí),是通過底層的C,C++和匯編實(shí)現(xiàn)的,其中Runtime的使用方式有三種:
第一種:通過OC代碼,例如 [person sayNB]
第二種:通過NSObject里面的方法,例如isKindOfClass
第三種:通過Runtime API,例如class_getInstanceSize
說到這里, 我覺得附上一張關(guān)系圖能讓你們更加清晰

了解Runtime之后,我們接下來玩一個(gè)有意思的東西

我們把代碼編譯后,來到 clang,把里面的代碼實(shí)現(xiàn)拿到main函數(shù)里面,運(yùn)行,發(fā)現(xiàn)結(jié)果和OC的上層調(diào)用方法是一摸一樣的,從而可以看出,我們的方法到了底層之后,就是 通過objc_msgSend消息發(fā)送的
也可以通過msg搜索把Enable Strict Checking of objc_ msgSend Calls嚴(yán)厲機(jī)制關(guān)了,用objc_msgSend來發(fā)送消息,就像這樣

效果等同于[person1 sayNB];,除了這一點(diǎn),我們還可以用person1的調(diào)用執(zhí)行父類中實(shí)現(xiàn),通過objc_msgSendSuper來實(shí)現(xiàn)

在這個(gè)過程中,我們的子類是沒有實(shí)現(xiàn) sayhello的,所以從中可以看出 [person sayHello]和 objc_msgSendSuper執(zhí)行的都是父類中的 sayHello。
那么問題來了,objc_msgSend是怎么找到我們的方法的呢? 現(xiàn)在我們明白了OC上層調(diào)用的方法,到了下層是發(fā)送消息。 消息里有sel和imp,通過sel方法編號綁定imp,imp就是我們的函數(shù)指針地址,通過imp我們可以找到具體的內(nèi)容。那么我們怎么通過sel找到imp呢, 這就是本篇文章關(guān)注的重點(diǎn)
objc_msgSend 快速查找流程分析
由于C和C++查找的話比匯編慢了一丟丟,為了提高性能和方法的動態(tài)性,sel找imp是通過匯編找的。聽到匯編不要緊張,不要驚慌,看不懂匯編沒關(guān)系,我會標(biāo)好注釋,我們只需要知道流程即可
我們先找到我們的匯編代碼,通過xcode搜索objc_msgsend,由于是匯編寫的,所以找后綴.s 的文件,然后我們常用的架構(gòu)又是arm64,所以最終找到了objc-msg-arm64.s里面

通過 ENTRY _objc_msgSend找到底層的匯編源碼

在講這個(gè)代碼之前,我們要始終記住一點(diǎn),就是我們一直都在通過sel找imp,記住這一點(diǎn)之后,我們分析代碼的時(shí)候不會迷路
好了,開始我們的匯編分析之旅:
// _objc_msgSend 入口
ENTRY _objc_msgSend
// 無窗口
UNWIND _objc_msgSend, NoFrame
// cmp 是對比的意思, p0 是objc_msgSend的第一個(gè)參數(shù)。意思是p0先和空做對比,如果為空就沒有必要往下走了。
cmp p0, #0 // nil check and tagged pointer check
// le小于 --支持taggedpointer(小對象類型)的流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// p0 等于 0 時(shí),直接返回 空
b.eq LReturnZero
#endif
// 根據(jù)對象拿出isa
ldr p13, [x0] // p13 = isa
// 根據(jù)isa 拿出類
GetClassFromIsa_p16 p13 // p16 = class
// 獲取isa完畢
LGetIsaDone:
// CacheLookup,從緩存里面獲取imp的流程,也就是我們今天探討的重點(diǎn),從sel-imp快速查找流程
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
#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
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
到這一步我相信都很容易理解,主要是拿到類信息,即class,拿到class之后來到CacheLookup從緩存里面獲取imp的流程。
我們先來看如何拿到class的,先找到macro GetClassFromIsa_p16,然后看源碼實(shí)現(xiàn)

有一說一,#if SUPPORT_INDEXED_ISA里面的這些匯編我也看不太懂,參考了高人的指點(diǎn)。不過我們也不需要看懂,因?yàn)槲覀?code>arm64真機(jī)不會走到那里面去,我們走的是#elif __LP64__里面,這里面就很好理解了, 直接拿isa & ISA_MASK 就可以獲取到類信息,這個(gè)在之前的文章已經(jīng)講過。
好了,獲取class完成后,接下來我們重點(diǎn)分析CacheLookup, 我們首先找到macro CacheLookup,注意,在當(dāng)前類里面找,別找到別人家去了
.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:
// p1 = SEL, p16 = isa
// CACHE是一個(gè)宏定義, 2 * 8 = 16 #define CACHE (2 * __SIZEOF_POINTER__)
// p16是isa,isa位移16個(gè)字節(jié)得到我們的cache,然而cache的首地址又是mask_buckets
// 然后把cache又放到p11里面
ldr p11, [x16, #CACHE] // p11 = mask|buckets
// ------------ arm64位真機(jī)
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets,存到p10 = buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// LSR:p11(cache)邏輯右移48位,拿到mask。 然后 mask & p1(sel & mask) ,得到sel-imp的下標(biāo)index(即前面講過的cache_hash的下標(biāo)) 存入p12
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
// --------- 非arm64位真機(jī) 這些就不用看了
#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
// p12是獲取的下標(biāo),然后邏輯左移4位,相當(dāng)于是(下標(biāo) * 16(16是sel和imp的大小)),再由p10(buckets)通過內(nèi)存平移的方式得到bucket保存到p12中
add p12, p10, p12, LSL #(1+PTRSHIFT) = (1 + 3) = 4
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 得到bucket之后通過指針地址得到{imp, sel},然后將imp 和 sel分別賦值為p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 接下來就是開始循環(huán)了, 判斷當(dāng)前bucket的 sel 與 p1(傳入的sel)是否相等
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 如果不相同,則跳入2f
b.ne 2f // scan more
// 如果相同直接返回imp
CacheHit $0 // call or return imp
// 沒有找到 進(jìn)入2f, 如果一直都找不到, 因?yàn)槭莕ormal ,跳轉(zhuǎn)至__objc_msgSend_uncached
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
// 判斷p12(下標(biāo)對應(yīng)的bucket)是否 等于 p10(buckets數(shù)組第一個(gè)元素),如果等于,則跳轉(zhuǎn)到3f
cmp p12, p10 // wrap if bucket == buckets
// 如果相等 跳入3f
b.eq 3f
// 因?yàn)橐獙12的指針指到buckets的最后一個(gè)元素后,所以進(jìn)行了bucket--, 向前一直查找下去
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
// 跳轉(zhuǎn)至第1步,遞歸繼續(xù)對比 sel 與 cmd
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// p11(mask)右移44位 相當(dāng)于mask左移4位,直接定位到buckets的最后一個(gè)元素
// 注意,這個(gè)地方會往下面走,不會再回去了
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// 把當(dāng)前查詢的bucket人為設(shè)為最后一個(gè)元素給了p12,然后會往下走
// p12 = buckets + (mask << 1+PTRSHIFT)
// 這個(gè)地方不是真機(jī)環(huán)境,不用看
#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.
// 上面步驟之后然后在繼續(xù)查找,拿到x12(即p12)bucket中的 imp-sel 分別存入 p17-p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 比較 sel 與 p1(傳入的參數(shù)cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 如果不相等,即走到2f
b.ne 2f // scan more
// 如果相等 即命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
// 如果一直找不到,則CheckMiss
CheckMiss $0 // miss if bucket->sel == 0
// 判斷p12(bucket)是否 等于 p10(buckets數(shù)組第一個(gè)元素)-- 表示前面已經(jīng)沒有了,但是還是沒有找到
cmp p12, p10 // wrap if bucket == buckets
// 如果等于,跳轉(zhuǎn)至第3步
b.eq 3f
// 從p12 buckets首地址 - 實(shí)際需要平移的內(nèi)存大小BUCKET_SIZE,得到得到第二個(gè)bucket元素,imp-sel分別存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
// 跳轉(zhuǎn)至第1步,繼續(xù)對比 sel 與 cmd
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
// 跳轉(zhuǎn)至JumpMiss 因?yàn)槭莕ormal ,跳轉(zhuǎn)至__objc_msgSend_uncached
JumpMiss $0
.endmacro
看完這一大篇匯編代碼,有的同學(xué)可能不太明白,我也是費(fèi)了老大功夫?qū)戇@個(gè)注釋??床欢畢R編沒關(guān)系,流程弄明白再百度匯編語法慢慢看。
我們來總結(jié)一下,我相信我總結(jié)的能夠讓你更加清晰整個(gè)流程,把流程弄清楚再來看匯編也許感覺就不一樣了。
主要分為以下六步:
1、拿到類的isa之后通過內(nèi)存平移16個(gè)字節(jié),找到我們的cache,就是匯編中的p11
2、從cache里面取出buckets和mask,通過cmd & mask 拿到哈希下標(biāo)index存入p12
3、根據(jù)所得的哈希下標(biāo)index 和 buckets內(nèi)存平移,取出哈希下標(biāo)對應(yīng)的bucket
4、 判斷bucket的sel 與 傳入的參數(shù)cmd是否相等,如果相等直接返回imp,如果不相等則判斷是否是第一個(gè)元素,如果是,把當(dāng)前bucket人為設(shè)為最后一個(gè)元素,進(jìn)入到第五個(gè)步驟。 如果不是則{imp, sel} = *--bucket,遞歸向前查找回到第四個(gè)步驟,繼續(xù)進(jìn)行對比
注意,人為設(shè)定到最后一個(gè)元素只有一次,設(shè)定完就會到第五個(gè)步驟
5、找到了第一個(gè)元素,人為設(shè)定到最后一個(gè)元素之后:再遞歸向前查找:比較 sel 與 傳入的參數(shù)cmd 是否相等,如果不相等,繼續(xù)向前查找,直到找到sel等于cmd,返回imp。
6、 如果步驟4和步驟5兩次遞歸完了后還沒找到則會跳到__objc_msgSend_uncached,進(jìn)入慢速查找流程,我們下篇文章再進(jìn)行分析。
最后,再附上一張objc_msgSend快速查找流程圖結(jié)束今天的內(nèi)容
