OC底層探究之從cache到objc_msgSend

1、cache插入流程分析

《OC底層探究之類的cache_t分析》中探索了方法是如何存入類中的緩存的!可是方法是什麼時候存入類的緩存的呢?html

接下來咱們就開始探索方法是什麼時候插入緩存的!java

先打開objc源碼,在調用方法處打上斷點c++

image-20210627173608622

等斷住後,在insert方法中打上斷點,斷住後在lldb中使用bt查看堆棧算法

image-20210627173749055

在堆棧中能夠看到從對象調用方法進入到insert方法的整個流程:編程

_objc_msgSend_uncached -> lookUpImpOrForward -> log_and_fill_cache -> insertapi

這裏能夠看到對象調用方法的時候最早調用的是objc_msgSend方法!緩存

objc_msgSend方法就涉及到了runtimemarkdown

2、runtime的運行時理解

一、靜態編程和動態編程

首先咱們要先知道編程語言有靜態和動態之分。架構

所謂靜態語言,就是在程序運行前決定了全部的類型判斷,類的全部成員、方法在編譯階段(即編譯時)就肯定好了內存地址。也就意味着全部類對象只能訪問屬於本身的成員變量和方法,不然編譯器直接報錯。比較常見的靜態的語言如:java,c++,c等等。app

而動態語言,偏偏相反,類型的判斷、類的成員變量、方法的內存地址都是在程序的運行階段(即運行時)才最終肯定,而且還能動態的添加成員變量和方法。也就意味着你調用一個不存在的方法時,編譯也能經過,甚至一個對象它是什麼類型並非表面咱們所看到的那樣,只有運行以後才能決定其真正的類型。相比於靜態語言,動態語言具備較高的靈活性和可訂閱性。而oc,正是一門動態語言。

二、編譯時

編譯時顧名思義就是正在編譯的時候。那啥叫編譯呢?就是編譯器幫你把源代碼翻譯成機器能識別的代碼。(固然只是⼀般意義上這麼說,實際上可能只是翻譯成某個中間狀態的語⾔。)

那麼編譯時就是簡單的做⼀些翻譯⼯做,⽐如檢查⽼兄你有沒有粗⼼寫錯啥關鍵字了啊、詞法分析、語法分析之類的過程。 就像個⽼師檢查學⽣的做⽂中有沒有錯別字和病句⼀樣 。若是發現啥錯誤編譯器就告訴你。

若是你⽤微軟的VS的話,點下build.那就開始編譯,若是下⾯有errors或者warning信息,那都是編譯器檢查出來的。所謂這時的錯誤就叫編譯時錯誤,這個過程當中作的啥類型檢查也就叫編譯時類型檢查,或靜態類型檢查(所謂靜態嘛就是沒把真把代碼放內存中運⾏起來,⽽只是把代碼看成⽂原本掃描下)。

因此有時⼀些⼈說編譯時還分配內存啥的確定是錯誤的說法

三、運行時

運⾏時,就是代碼跑起來了,被裝載到內存中去了。

你的代碼保存在磁盤上沒裝⼊內存以前是個死傢伙,只有跑到內存中才變成活的。⽽運⾏時類型檢查就與前⾯講的編譯時類型檢查(或者靜態類型檢查)不⼀樣.不是簡單的掃描代碼。⽽是在內存中作些操做,作些判斷。

關於運行時更多詳細的內容能夠去官方文檔上進行查閱。(Objective-C 運行時編程指南

四、runtime發起方式

一、OC的方法。

二、NSObject的方法。

三、objc動態庫的api。

能夠經過一個圖來表示層級:

runtime層級

五、運行時和編譯時的區別

建立一個對象,有兩個方法,只實現一個方法,運行一下:

image-20210629114546717

就會發現,編譯是成功的,可是一運行就報錯了,這就是編譯時運行時的區別!

六、經過底層分析

接下來咱們經過clang的還原,看看OC代碼在底層的實現,找到main函數:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        HPerson * p = ((HPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("saySix"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));
    }
    return 0;
}
複製代碼

發現編譯後上層的代碼都會獲得一個解釋

調用方法的過程就是調用objc_msgSend函數,即消息發送!

經過底層代碼咱們能夠發現objc_msgSend函數有2個參數:

一個是(id)objc_getClass("HPerson")或者(id)p,即消息的接受者!

一個是sel_registerName("xxx")sel

如今調用的方法都是沒有帶參數的,若是是帶有參數的呢?

咱們加上參數: image-20210629120214511

clang一下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        HPerson * p = ((HPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("saySomething:"), (NSString *)&__NSConstantStringImpl__var_folders_1h_55lzq4fd39b0mz94wmqthqf860mpgy_T_main_189844_mi_2);
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("saySix"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));
    }
    return 0;
}
複製代碼

能夠看到多了一個NSString參數!

因此能夠得出消息發送方式

objc_msgSend (消息接受者, 消息主體(sel + 參數))

七、底層代碼調用

那咱們是否能夠直接在代碼中進行調用呢?

在調用以前先在Build Setting中將Enable Strict Checking of objc_msgSend Calls改成NO:

image-20210629140657255

意思是對調用objc_msgSend函數的要求放寬,由咱們來決定參數!

而後咱們嘗試直接在代碼中調用:

image-20210629140948759

發現和咱們正常使用代碼調用方法是如出一轍的!

八、NSObject方法調用

那麼NSObject有是怎麼調用的呢?

進入到NSObject.h中就能夠看到相關方法了:

image-20210629151359255

很明顯performSelector方法有關,咱們來嘗試一下:

image-20210629151650360

和咱們正常調用是同樣的!

3、查看objc_msgSend源碼

一、彙編調試

先打開彙編image-20210629173430049

而後在方法前打上斷點,運行:

image-20210629173558884

objc_msgSend處打上斷點,斷住後,按住ctrl點擊step into

image-20210629173828894

便可發現objc_msgSend是來自objc底層源碼!

二、查看源碼

打開objc源碼,搜索objc_msgSend

image-20210629174130568

由於是objc_msgSend的底層是彙編寫的,因此咱們直接看.s文件!

由於咱們用的最多仍是真機,因此咱們看arm64的:

image-20210629174445925

找到ENTRY _objc_msgSend

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		// (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
複製代碼

咱們會發現_objc_msgSend是用彙編寫的,而咱們以前的源碼都是用c或者c++寫的,這是爲何呢?

由於彙編,並且具備動態化

4、彙編源碼分析

一、彙編的常見指令

b 指令

  • bl 跳轉到標號出執行
  • b.le 判斷上面cmp的值是小於等於 執行標號,不然直接往下走
  • b.ge 大於等於 執行地址 不然往下
  • b.lt 判斷上面camp的值是 小於 執行後面的地址中的方法 不然直接往下走
  • b.gt 大於 執行地址 不然往下
  • b.eq 等於 執行地址 不然往下
  • b.hi 比較結果是無符號大於,執行地址中的方法,不然不跳轉
  • b.hs 指令是判斷是否無符號小於
  • b.ls 指令是判斷是否無符號大於
  • b.lo 指令是判斷是否無符號大於等於

ret 返回

  • mov x0,#0x10 -> x0 = 0x10
  • str w10 ,[sp] 將w10寄存器的值存到 sp棧空間內存
  • stp x0,x1,[sp.#0x10]* : x0、x1 的值存入 sp + 0x10
  • orr x0,wzr,#0x1 : x0 = wzr | 0x1
  • stur w10 ,[sp] 將w10寄存器的值存到 sp棧空間內存
  • ldr w10 ,[sp] w10 = sp棧內存中的值
  • ldp x0,x1,[sp] x0、x1 = sp棧內存中的值

adrp 經過基地址 + 偏移 得到一個字符串(全局變量)

  • cbz 比較,爲零則跳轉;
  • cbnz: 比較,爲非零則跳轉。
  • cmp: 比較功能 例如 : cmp OPR1 , OPR2. = (OPR1)-(OPR2)

二、開始分析

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

搜索p0

#if __LP64__
// true arm64

#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
#define p0 x0
#define p1 x1
#define p2 x2
#define p3 x3
#define p4 x4
#define p5 x5
#define p6 x6
#define p7 x7
#define p8 x8
#define p9 x9
#define p10 x10
#define p11 x11
#define p12 x12
#define p13 x13
#define p14 x14
#define p15 x15
#define p16 x16
#define p17 x17

// true arm64
#else
// arm64_32

#define SUPPORT_TAGGED_POINTERS 0
#define PTR .long
#define PTRSIZE 4
#define PTRSHIFT 2 // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTW
#define p0 w0
#define p1 w1
#define p2 w2
#define p3 w3
#define p4 w4
#define p5 w5
#define p6 w6
#define p7 w7
#define p8 w8
#define p9 w9
#define p10 w10
#define p11 w11
#define p12 w12
#define p13 w13
#define p14 w14
#define p15 w15
#define p16 w16
#define p17 w17

// arm64_32
#endif
複製代碼

能夠發現p0,是指寄存器x0!即咱們傳入的第一個參數p

這條指令的意思就讓p00進行對比,判斷p0是否爲

即判斷消息接收者是否存在,若是不存在則:

#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		// (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
複製代碼

SUPPORT_TAGGED_POINTERS1時,進入LNilOrTagged

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
複製代碼

SUPPORT_TAGGED_POINTERS0時相似,最後都是執行LReturnZero

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

即把寄存器清空,而後結束_objc_msgSend

正常狀況固然是繼續往下走:

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

x0賦值給p13x0便是消息接受者,即傳入的第一個參數p

註釋代表是將isa賦值給p13,爲何是isa呢?

由於isap首地址

三、獲取class-GetClassFromIsa_p16

繼續:

GetClassFromIsa_p16 p13, 1, x0	// p16 = class
複製代碼

看註釋代表是將class賦值給了p16

開始探索GetClassFromIsa_p16

調用GetClassFromIsa_p16,並將p131x0傳入。

進入GetClassFromIsa_p16

/******************************************************************** * GetClassFromIsa_p16 src, needs_auth, auth_address * src is a raw isa field. Sets p16 to the corresponding class pointer. * The raw isa might be an indexed isa to be decoded, or a * packed isa that needs to be masked. * * On exit: * src is unchanged * p16 is a class pointer * x10 is clobbered ********************************************************************/
 
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
	// Indexed isa
	mov	p16, \src			// 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__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro
複製代碼

.macro表示這是一個宏定義!

先看SUPPORT_INDEXED_ISA

// Define SUPPORT_INDEXED_ISA=1 on platforms that store the class in the isa 
// field as an index into a class table.
// Note, keep this in sync with any .s files which also define it.
// Be sure to edit objc-abi.h as well.
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
複製代碼

如今基本都是64位,因此咱們主要看SUPPORT_INDEXED_ISA0的狀況,以及爲__LP64__的狀況:

.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
複製代碼

needs_auth爲第二個參數,即1!

因此走的是ExtractISA,傳入p16p13(isa)x0

.macro ExtractISA	and    $0, $1, #ISA_MASK.endmacro
複製代碼

這裏是讓$1#ISA_MASK相加,而後賦值給了$0

這裏和咱們以前看的源碼很像!即isa&ISA_MASK,獲得class

因此這一步就將class賦值給了p16

爲何要把class取出來呢?由於cache是在class裏面,取出class後就要準備插入緩存了!

而後結束GetClassFromIsa_p16

四、查找緩存-CacheLookup

4.一、總覽

獲取了class後:

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

接着就進入到了CacheLookup,即查找緩存:

/******************************************************************** * * CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant * * MissLabelConstant is only used for the GETIMP variant. * * Locate the implementation for a selector in a class method cache. * * When this is used in a function that doesn't hold the runtime lock, * this represents the critical section that may access dead memory. * If the kernel causes one of these functions to go down the recovery * path, we pretend the lookup failed by jumping the JumpMiss branch. * * Takes: * x1 = selector * x16 = class to be searched * * Kills: * x9,x10,x11,x12,x13,x15,x17 * * Untouched: * x14 * * On exit: (found) calls or returns IMP * with x16 = class, x17 = IMP * In LOOKUP mode, the two low bits are set to 0x3 * if we hit a constant cache (used in objc_trace) * (not found) jumps to LCacheMiss * with x15 = class * For constant caches in LOOKUP mode, the low bit * of x16 is set to 0x1 to indicate we had to fallback. * In addition, when LCacheMiss is __objc_msgSend_uncached or * __objc_msgLookup_uncached, 0x2 will be set in x16 * to remember we took the slowpath. * So the two low bits of x16 on exit mean: * 0: dynamic hit * 1: fallback to the parent class, when there is a preoptimized cache * 2: slowpath * 3: preoptimized cache hit * ********************************************************************/

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	// As soon as we're past the LLookupStart\Function 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\Function,
	// then our PC will be reset to LLookupRecover\Function 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
	//

	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	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

	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
	cmp	p9, p1				// if (sel != _cmd) {
	b.ne	3f				// scan more
						// } else {
2:	CacheHit \Mode				// hit: call or return imp
						// }
3:	cbz	p9, \MissLabelDynamic		// if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	// p10 = first bucket
	// p11 = mask (and maybe other bits on LP64)
	// p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
	cmp	p9, p1				// if (sel == _cmd)
	b.eq	2b				// goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		// bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &= mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro
複製代碼

4.二、獲取buckets

首先看傳入的參數:NORMAL, _objc_msgSend, __objc_msgSend_uncached,一共3個參數。

分別對應Mode, Function, MissLabelDynamic, MissLabelConstant

可是這個函數須要傳入4個參數,說明最後一個爲默認值

而後跟着繼續往下走:

mov	x15, x16			// stash the original isa
複製代碼

這裏是將x16class賦值給了x15

繼續:

LLookupStart\Function:	// p1 = SEL, p16 = isa
複製代碼

Function爲傳入的_objc_msgSend,即開始_objc_msgSend

接着就來到了一個判斷

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	//...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	//...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	//...
#else
#error Unsupported cache mask storage for ARM64.
#endif
複製代碼

咱們先看看CACHE_MASK_STORAGE

#if defined(__arm64__) && __LP64__ 
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16 //真機64位
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
複製代碼

由於咱們主要看真機64位的模式,因此只須要看CACHE_MASK_STORAGE_HIGH_16便可:

ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
複製代碼

先看ldr,即存儲值,這裏是先把x16加上#CACHE,而後在賦值p11

找一下#define CACHE

#define CACHE (2 * __SIZEOF_POINTER__)
複製代碼

__SIZEOF_POINTER__是指針的大小,即CACHE16

因此是x16平移16字節,即class平移16字節獲得cache!再賦值給p11,即p11就是cache的首地址(即_bucketsAndMaybeMask)!

接着看CONFIG_USE_PREOPT_CACHES

#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif
複製代碼

真機時爲1!即只用看這一段:

#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
複製代碼

__has_feature:此函數的功能是判斷編譯器是否支持某個功能。

ptrauth_calls:指針身份驗證,針對 arm64e 架構;使用 Apple A12 或更高版本 A 系列處理器的設備(如 iPhone XSiPhone XS MaxiPhone XR 或更新的設備)支持 arm64e 架構。

咱們看大多數狀況,即A12如下的真機,即else部分!

先是把p11&0x0000fffffffffffe再賦值給了p10!那麼p10是什麼呢?

回顧獲取buckets的方法:

struct bucket_t *cache_t::buckets() const {
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}
複製代碼

這裏p11cache的首地址,即_bucketsAndMaybeMask,而0x0000fffffffffffe爲掩碼,因此p10buckets

拿到buckets後:

tbnz	p11, #0, LLookupPreopt\Function
複製代碼

tbnz:第0位不爲0則發生跳轉。

通常狀況buckets都是爲0的。

4.三、開始查找

繼續往下走:

eor	p12, p1, p1, LSR #7
and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
複製代碼

LSR:按位右移。

這裏和以前insert方法的獲取hash是同樣的:

static inline mask_t cache_hash(SEL sel, mask_t mask) {
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}
複製代碼

因此這裏是從新hash,獲取下標,即p12開始值(begin)

拿到哈希下標後:

add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
複製代碼

LSL:按位左移。

而後找PTRSHIFT

#if __LP64__
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
#else
#define PTRSHIFT 2 // 1<<PTRSHIFT == PTRSIZE
#endif
複製代碼

因此PTRSHIFT3

這裏p12hash下標,左移4位則p12的值乘以16即爲內存大小!

因此這裏是p10這個buckets進行內存平移取bucket

因此p13是當前查找的bucket

接着:

// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
	cmp	p9, p1				// if (sel != _cmd) {
	b.ne	3f				// scan more
						// } else {
2:	CacheHit \Mode				// hit: call or return imp
						// }
3:	cbz	p9, \MissLabelDynamic		// if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b
複製代碼

ldp x0,x1,[sp] :x0、x1 = sp棧內存中的值。

cbz :比較,爲零則跳轉。

分析:

1:將當前bucketimpsel分別賦值給p17p9,而後x13-16,即x13爲前一個bucket,接着用p9(sel)咱們傳進來的方法進行比較,若是不同則跳到3,若是同樣則跳到2

二、找到了咱們傳進來的方法,即緩存命中CacheHit

三、若是p9(sel)爲空,則進入MissLabelDynamicCacheLookup函數傳入的第三個參數__objc_msgSend_uncached,若是不爲空,判斷當前x13這個bucket地址是否大於等於p10這個buckets首地址,大於等於則跳轉到1,不然接着往下走!

3步就是一個do...while循環!

爲何p9(sel)爲空就miss呢?

一、首先取緩存和存緩存的hash算法是同樣的。

二、真機狀況下,存緩存的時候若是hash衝突了,則:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
複製代碼

說明真機是按位存儲緩存的,因此發現爲空說明緩存已經找完了,並且沒有找到,則miss!

同理,當bucket的地址小於buckets的地址時,也說明緩存已找完了,即跳出循環!

4.四、緩存命中CacheHit

當找到了咱們傳入的方法後,就會進入到緩存命中CacheHit

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro
複製代碼

$0CacheLookup函數傳入的第一個值,即爲NORMAL

因此只用看:

TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
複製代碼

接着進入TailCallCachedImp函數:

#if __has_feature(ptrauth_calls)
// JOP
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$1, $1, $2	// mix SEL into ptrauth modifier
	eor	$1, $1, $3  // mix isa into ptrauth modifier
	brab	$0, $1
.endmacro
#else
// not JOP
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$0, $0, $3
	br	$0
.endmacro
#endif
複製代碼

咱們看A12如下的狀況:

.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$0, $0, $3
	br	$0
.endmacro
複製代碼

eor:按位異或。

咱們傳入了x17(imp), x10(buckets), x1(傳入的sel), x16(isa)

而後把$0(imp)$3(isa即類)進行異或,再賦值給$0

這裏爲何要異或呢?

在咱們插入緩存的時候,進行了編碼

// Sign newImp, with &_imp, newSel, and cls as modifiers.
    uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        return (uintptr_t)
            ptrauth_auth_and_resign(newImp,
                                    ptrauth_key_function_pointer, 0,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
    }
複製代碼

因此這裏是解碼得到imp

最後跳轉imp

到這裏就是objc_msgSend經過sel查找imp的過程!

4.五、繼續查找

若是p13(bucket)p10(buckets)小,則跳出循環,繼續往下走:

// wrap-around:
	// p10 = first bucket
	// p11 = mask (and maybe other bits on LP64)
	// p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
複製代碼

只用看CACHE_MASK_STORAGE_HIGH_16

add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
複製代碼

這裏maskbuckets開闢的內存-1,因此這裏是把buckets右移mask個內存!

因此p13buckets的最後一個bucket

接着:

add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket
複製代碼

這裏是p12(begin)左移4位,即經過hash獲得的begin * 16,而後再把p10(buckets)右移p12大小的內存。

因此p12爲以前hash獲得的begin處的bucket

繼續:

// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
	cmp	p9, p1				// if (sel == _cmd)
	b.eq	2b				// goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		// bucket > first_probed)
	b.hi	4b
複製代碼

這裏上面的循環相似:

一、先賦值impselp17p9,而後bucket--

二、比較p9(sel)傳入的sel比較,相同則進入緩存命中CacheHit

三、比較p9(sel)是否不爲0,並且bukcet是否大於p12(begin處的bucket)

四、知足則跳回4(循環的開始處)。

4.六、沒有找到

若是p9(sel)0,或者bukcet小於p12(begin處的bucket),則繼續:

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic
複製代碼

這裏就是沒有找到緩存,進入MissLabelDynamic(__objc_msgSend_uncached)

5、總結

objc_msgSend

相關文章
相關標籤/搜索