iOS底層原理探索-07- Runtime之消息查找

《目錄-iOS & OpenGL & OpenGL ES & Metal》緩存

既然知道了方法的本質就是發送消息,那咱們繼續研究一下runtime的消息查找markdown

前言

runtime的消息查找分爲2步:多線程

  • 快速查找流程
  • 慢速查找流程

1、objc_msgSend

objc_msgSend是用匯編寫的,那咱們就從彙編開始探索一下objc_msgSend都作了些什麼函數

延伸:爲何objc_msgSend是用匯編而不是用C編寫的呢?oop

  • 是由於:
    • c語言不能經過 只寫一個函數,而後保留未知參數,就跳轉到任意的指針。
    • 彙編有寄存器(arm64下有31個寄存器,每個表明64位)
    • 彙編更容易能被機器識別,對於一些調用頻率過高的函數或操做,使用匯編來實現可以提升效率和性能

一、開始探索objc-msg-arm64.s

來到源碼中,找到objc-msg-arm64.s,再找到ENTRY _objc_msgSendpost

個人天啊!這都是些什麼鬼?性能

不要緊,我也看不懂,咱們邊百度指令,邊分析註釋,硬讀吧~spa

二、GetClassFromIsa_p16

搜索看一下這個方法,是怎麼經過isa拿到class的: 的確看到了熟悉的代碼ISA_MASK,就是經過isa & ISA_MASK運算,拿到class,而後走到 CacheLookup方法線程

2、快速流程

一、CacheLookup

先看一下注釋內存,咱們通常用到都是NORMAL 模式。這時,咱們已經拿到了selclass指針

(吐槽一下,這段代碼太長了,截個圖費勁巴拉的) 咱們上一段代碼分析一段:

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 & maskcache_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
p12imp賦值給p17sel賦值給p9

1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
複製代碼

比較p9p1,就是對比咱們傳進來的方法編號,是否和緩存中找到的匹配,匹配就是緩存命中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方法
p10p12進行比較,若是eq 相等,就跳到第三步
若是不相等,p12的指針進行--操做,拿到新的selimp
再跳到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方法

二、CheckMissJumpMiss

cbz比較,若是結果爲0就跳轉後面
由於咱們是 NORMAL 模式,因此無論進哪一個方法都會來到 __objc_msgSend_uncached方法

三、__objc_msgSend_uncached

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

四、MethodTableLookup

大體一看,又要計算?咱們直接抓住核心的點:bl _lookUpImpOrForward,跳轉了這個方法,全局搜索一下_lookUpImpOrForward發現並無。那搜索一下lookUpImpOrForward,有這個方法!

其實,咱們這裏去掉下劃線找方法,屬於開啓了上帝視角。若是按正常流程,咱們應該打開彙編,斷點方法,看彙編裏面的jumpcallq命令都走了哪些方法

由於lookUpImpOrForward這個方法是一個C/C++方法,它的參數必須是肯定的,這樣就能夠解釋通bl _lookUpImpOrForward這行代碼前面的操做了,就是爲了傳入肯定的參數作準備。

快速流程就到這裏,就是在緩存中經過sel找imp,找不到就進入慢速流程。

3、慢速流程

咱們在上一篇章中驗證過,方法存儲在 類 -> bit -> rw -> ro -> methodList裏面,帶着這個思路,看一看runtime在源碼中查找消息的流程是否是如出一轍的?

一、lookUpImpOrForward

哇,這裏面又臭又長,咱們就簡述一下,講一下幾個須要注意的點:

  • 有一步容錯,經過cache_getImp方法,若是找到了imp就直接返回

  • 細節點

    • runtimeLock.lock();在這裏加鎖,防止同時訪問2個方法,出現imp返回錯誤
    • checkIsKnownClass(cls);判斷這個類是不是被編譯過的,若是不是就輸出錯誤信息
    • 這裏還有一些準備工做,拿到對應的類和元類的信息
  • 若是是對象方法,就在當前類的方法列表中使用二分查找法尋找imp,找到就進行緩存,而後返回imp

    • 若是是類方法,就在當前的元類中找
  • 判斷,若是當前類,沒有父類,直接崩潰並打印錯誤信息

  • 若是有父類,直接在父類的緩存中找

    • 找到就緩存並返回imp
    • 找不到就在父類的方法列表中使用二分查找法尋找imp,找到就進行緩存,而後返回imp
  • 若是父類中沒有,就在元類(父類的父類)中找,繼續上一步的循環

    • 當前類 -> 父類 -> 元類 -> 根元類(NSObject)
  • 最後都沒找到,就進行動態方法解析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);
}
複製代碼

這個找到最後,好熟悉!這不就是找不到實現方法崩潰,在控制檯打印的信息嗎!!!

4、總結

一、對象方法查找流程

  • 對象的實例方法 - 本身有
  • 對象的實例方法 - 本身沒有 - 找父類的
  • 對象的實例方法 - 本身沒有 - 父類也沒有 - 找父類的父類 - NSObject
  • 對象的實例方法 - 本身沒有 - 父類也沒有 - 找父類的父類 - NSObject也沒有 - 崩潰

二、類方法查找流程

  • 類方法 - 本身有

  • 類方法 - 本身沒有 - 找父類的

  • 類方法 - 本身沒有 - 父類也沒有 - 找父類的父類 - NSObject

  • 類方法 - 本身沒有 - 父類也沒有 - 找父類的父類 - NSObject也沒有 - 崩潰

  • 類方法 - 本身沒有 - 父類也沒有 - 找父類的父類 - NSObject也沒有 - 可是有對象方法

三、消息查找流程

消息查找階段:

  1. 首先進入快速流程,拿到isa,經過彙編的手段在緩存中找,找到就返回
  2. 而後進入慢速流程,經過:當前類.方法列表 -> 父類.緩存 -> 父類.方法列表 -> 元類.緩存 -> 元類.方法列表 這個流程,哪一步找到就返回
  3. 最後都沒找到,進入``消息轉發階段`
相關文章
相關標籤/搜索