既然知道了方法的本質就是發送消息,那咱們繼續研究一下runtime的消息查找markdown
runtime的消息查找分爲2步:多線程
objc_msgSend
是用匯編寫的,那咱們就從彙編開始探索一下objc_msgSend
都作了些什麼函數
延伸:爲何
objc_msgSend
是用匯編而不是用C編寫的呢?oop
objc-msg-arm64.s
來到源碼中,找到objc-msg-arm64.s
,再找到ENTRY _objc_msgSend
。post
個人天啊!這都是些什麼鬼?性能
不要緊,我也看不懂,咱們邊百度指令,邊分析註釋,硬讀吧~spa
GetClassFromIsa_p16
搜索看一下這個方法,是怎麼經過isa拿到class的: 的確看到了熟悉的代碼
ISA_MASK
,就是經過isa & ISA_MASK
運算,拿到class,而後走到 CacheLookup
方法線程
CacheLookup
先看一下注釋內存,咱們通常用到都是
NORMAL
模式。這時,咱們已經拿到了sel
和class
指針
(吐槽一下,這段代碼太長了,截個圖費勁巴拉的) 咱們上一段代碼分析一段:
ldr p11, [x16, #CACHE] // p11 = mask|buckets
複製代碼
x16
將類對象內存地址平移16位賦值給p11。咱們以前研究過,平移16位恰好就是緩存cache。其實後面註釋就有了解釋
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
複製代碼
p11
經過and = &
與運算,拿到緩存中的buckets
賦值給p10
p11
經過LSR
右移48位,獲得mask
,和sel
進行與運算,賦值給p12
_cmd & mask
和cache_t
中的cache_hash
方法同樣,通過哈希運算以後,獲得了 bucket 結構體指針add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
複製代碼
p12
經過LSL
左移(1+PTRSHIFT)
,而後和p10
進行與運算,賦值給p12
把p12
的imp
賦值給p17
,sel
賦值給p9
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
複製代碼
比較p9
和p1
,就是對比咱們傳進來的方法編號,是否和緩存中找到的匹配,匹配就是緩存命中CacheHit
返回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
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
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
複製代碼
若是bucket->sel == 0
,就跳到CheckMiss
方法
p10
和p12
進行比較,若是eq 相等
,就跳到第三步
若是不相等,p12
的指針進行--
操做,拿到新的sel
、imp
再跳到1第一步,進入循環,從新執行一遍
// 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
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
複製代碼
這裏再執行一遍1~3的流程,至關於給了一個容錯的機會,若是第二次仍是找不到咱們須要的sel
對應的imp
,就跳到JumpMiss
方法,開始進入慢速流程。
咱們再看一下流程中間碰見CheckMiss
方法、JumpMiss
方法
CheckMiss
和JumpMiss
cbz
比較,若是結果爲0就跳轉後面
由於咱們是 NORMAL
模式,因此無論進哪一個方法都會來到 __objc_msgSend_uncached
方法
__objc_msgSend_uncached
__objc_msgSend_uncached
方法中最核心的邏輯就是 MethodTableLookup
方法,意爲查找方法列表。
MethodTableLookup
大體一看,又要計算?咱們直接抓住核心的點:bl _lookUpImpOrForward
,跳轉了這個方法,全局搜索一下_lookUpImpOrForward
發現並無。那搜索一下lookUpImpOrForward
,有這個方法!
其實,咱們這裏去掉下劃線找方法,屬於開啓了上帝視角。若是按正常流程,咱們應該打開彙編,斷點方法,看彙編裏面的jump
和callq
命令都走了哪些方法
由於lookUpImpOrForward
這個方法是一個C/C++方法,它的參數必須是肯定的,這樣就能夠解釋通bl _lookUpImpOrForward
這行代碼前面的操做了,就是爲了傳入肯定的參數作準備。
快速流程就到這裏,就是在緩存中經過sel找imp,找不到就進入慢速流程。
咱們在上一篇章中驗證過,方法存儲在 類 -> bit -> rw -> ro -> methodList
裏面,帶着這個思路,看一看runtime在源碼中查找消息的流程是否是如出一轍的?
lookUpImpOrForward
哇,這裏面又臭又長,咱們就簡述一下,講一下幾個須要注意的點:
有一步容錯
,經過cache_getImp
方法,若是找到了imp
就直接返回
細節點
runtimeLock.lock();
在這裏加鎖,防止同時訪問2個方法,出現imp返回錯誤checkIsKnownClass(cls);
判斷這個類是不是被編譯過的,若是不是就輸出錯誤信息若是是對象方法,就在當前類的方法列表中使用二分查找法
尋找imp
,找到就進行緩存,而後返回imp
判斷,若是當前類,沒有父類,直接崩潰並打印錯誤信息
若是有父類,直接在父類的緩存中找
imp
二分查找法
尋找imp
,找到就進行緩存,而後返回imp
若是父類中沒有,就在元類(父類的父類)中找,繼續上一步的循環
最後都沒找到,就進行動態方法解析resolveMethod_locked(inst, sel, cls, behavior);
在上面的流程中有一點要提一下,_objc_msgForward_impcache
這個方法,咱們去看一下~
c函數沒找到這個方法,彙編中找到了。
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
複製代碼
繼續找__objc_msgForward
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
複製代碼
c函數__objc_forward_handler
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製代碼
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
複製代碼
這個找到最後,好熟悉!這不就是找不到實現方法崩潰,在控制檯打印的信息嗎!!!
類方法 - 本身有
類方法 - 本身沒有 - 找父類的
類方法 - 本身沒有 - 父類也沒有 - 找父類的父類 - NSObject
類方法 - 本身沒有 - 父類也沒有 - 找父類的父類 - NSObject也沒有 - 崩潰
類方法 - 本身沒有 - 父類也沒有 - 找父類的父類 - NSObject也沒有 - 可是有對象方法
消息查找階段:
快速流程
,拿到isa
,經過彙編
的手段在緩存
中找,找到就返回慢速流程
,經過:當前類.方法列表 -> 父類.緩存 -> 父類.方法列表 -> 元類.緩存 -> 元類.方法列表 這個流程,哪一步找到就返回