??在OC端,所有的方法調用,在編譯的時候都轉成了消息的轉發objc_msgSend
或者objc_msgSendSuper
。那么問題來了,objc_msgSend
是怎么找到方法并調用的能??
帶著這個問題,我們找到objc-818.2
的源碼,全局搜索一下,經過一番查找發現它的實現是在objc-msg-arm64.s
匯編文件中,從ENTRY
位置入手。
一、方法快速查找流程
匯編imp快速查找流程:
cmp p0, #0
// 首先是查看消息 receiver 接收者是否存在,如果不存在,再判斷是否支持SUPPORT_TAGGED_POINTERS
,支持的話就跳到LNilOrTagged
執行(),不支持就跳到LReturnZero
執行
ldr p13, [x0]
// p13拿到isa
GetClassFromIsa_p16 p13, 1, x0
// 通過isa拿到class放入p16寄存器
GetClassFromIsa_p16 里面的ExtractISA
拿到class
.macro ExtractISA
and1, #ISA_MASK // $0 = isa & ISA_MASK = class
.endmacro
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
LGetIsaDone
開始calls IMP或者調用objc_msgSend_uncached
mov x15 , x16
// 保存class
- 調用函數LLookupStart\Function,根據架構選型,arm64的真機是走
CACHE_MASK_STORAGE = CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE]
// 獲取到cache_t存在p11寄存器
- CACHE 是在類結構體中的偏移 是 (2 * SIZEOF_POINTER) = 16字節,p11 = x16 + 0x10 得到 => cache_t
- CONFIG_USE_PREOPT_CACHES = 1
- __has_feature(ptrauth_calls) A12之后,也就是iPhone X之后
tbnz p11, #0, LLookupPreopt\Function
// 判斷cache_t是否存在
and p10, p11, #0x0000ffffffffffff
// 因為arm64真機的buckets是存在低48位,所以p11 (也就是cache_t) & 掩碼 (#0x0000ffffffffffff) = buckets- buckets是存bucket_t結構體的一段連續的內存空間,但是不是數組
eor p12, p1, p1, LSR #7
- p1存的是_cmd,LSR表示:右移7位,eor表示:異或
代碼意思就是: p12 = p1 ^ (p1 >> 7) ==> p12 = _cmd ^ (_cmd >> 7),意思就是對sel進行hash,將哈希之后的值存在p12寄存器中
and p12, p12, p11, LSR #48
- p11存cache_t,p12存sel的哈希值,cache_t在arm64中低48位是buckets,高16位是mask
- 代碼的意思是: p12 & (p11 >> 48),p11 >> 48得到高16位的mask,轉化一下就是
sel的hash值 & mask
==(_cmd ^ (_cmd >> 7)) & mask
存在p12寄存器中,其實最終就會得到一個index下標在p12中
add p13, p10, p12, LSL #(1+PTRSHIFT)
- 此時p10存 buckets(這個其實是那個連續內存的首地址,但是不是數組),p12是index下標,在
__LP64__
和arm64中PTRSHIFT = 3。- 代碼的意思是:p13 = buckets + (index << (1+PTRSHIFT)) ==> buckets + (index << 4) 相當于首地址加上一個下標n(可以用數組的首地址加一個下標n來理解),就是指向buckets容器中的一個內存塊。
進入遍歷do.....while循環
7.11: ldp p17, p9, [x13], #-BUCKET_SIZE
- BUCKET_SIZE是一個bucket的大小,x13指向了中間的某一個bucket,-BUCKET_SIZE,就是向前挪了一個位置,{imp, sel} = *bucket--,讀取imp到p17和sel到p9寄存器中
7.2
cmp p9, p1
? ?b.ne 3f
- 如果p9(也就是sel)不等于p1(也就是_cmd),也就是沒有找到imp,則跳到
處執行
7.3
2: CacheHit \Mode
- 如果p9的sel與 p1的_cmd相等,也就是找到imp,那就調取CacheHit函數
7.4
3: cbz p9, \MissLabelDynamic
???cmp p13, p10
???b.hs 1b
- 如果p9 == 0,也就是sel == 0,則跳轉到 MissLabelDynamic函數,這個形參函數傳進來的值是__objc_msgSend_unCache。否則如果p13 > p10,也就是往前遍歷,如果還大于首地址,則繼續跳到
,繼續bucket--,繼續找sel和_cmd比較。
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
- 如果上面的循環沒有找到,也就是前一半遍歷沒有找到,則定位到后一半繼續遍歷。p10存 buckets,即首地址,p13 = buckets + (mask << 1+PTRSHIFT),p13定位到最后一個bucket_t
add p12, p10, p12, LSL #(1+PTRSHIFT)
- 在前一半遍歷的時候,p12存的一個index位置的bucket_t,所以后一半遍歷的時候,以p12往后一個作為起始點,以first_probed表示,防止重復遍歷前面的部分。
4: ldp p17, p9, [x13], #-BUCKET_SIZE
- p13已經定位到最后一個,然后循環取出bucket_t {imp, sel}存在p17(imp),和p9(sel)中。
cmp p9, p1
// if (sel == _cmd)
b.eq 2b
// goto cacheHit
cmp p9, #0
// } while (sel != 0 &&
ccmp p13, p12, #0, ne
// bucket > first_probed)
b.hi 4b
- 然后依次遍歷對比,如果sel == _cmd ,則跳到cacheHit執行,否則只要sel != 0 且p13與起始位置沒有碰面(bucket > first_probed)繼續循環遍歷。
小結:
??簡單總結一下,方法快速查找流程是用匯編實現的,其原因個人認為:
其一
、匯編實現能夠直接操作寄存器快速,而這部分是調用最多的更能提高速度。
其二
、是為了應對不同的調用轉換,因為所有的方法最后都轉化成objc_msgSend消息,其參數數量,參數類型,返回類型都不一樣,使用C、C++、Objective-C是不能做到,就算能做那也要寫龐大的代碼量才能實現,就談不上快速查找了,而使用匯編加上類型轉換,就可以實現了一套簡單的代碼完成所有的調用類型。
獲取到class和isa --->
通過偏移拿到cache_t--->
根據架構與上掩碼得到buckets和mask--->
定位index使用do...while遍歷前一半和后一半找sel與_cmd對比來查找imp--->
找到則CacheHit--->
找不到MissLabelDynamic,也就是__objc_msgSend_uncached(漫長的C/C++方法查找)
二、方法慢速查找初探
??我們注意到前面第7.4
步,如果找遍了所有的buckets都沒有找到imp,會調用MissLabelDynamic,在第4
步調起CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
的時候傳進來的值是__objc_msgSend_uncached。其源碼如下:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
會調用MethodTableLookup
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
然后bl _lookUpImpOrForward,這個就是方法的慢速查找流程,進入C/C++的方法查找流程和轉發。
小結:
??方法的慢速查找在另一個篇章iOS底層探索--方法慢速查找進行詳細講解,這里是為了能了解到底層是如何從匯編的快速查找無縫銜接到漫長的C/C++查找的。
??匯編的代碼分析是一個枯燥的過程,需要耐心,耐心,耐心,結合源碼的底層數據結構才能更好的理解,讀者可結合思路,自己自己的推敲一遍,你會有一種神清氣爽的感覺。
微風拂面,都仿佛對面走來的女孩在向你示愛!!!