OC底層原理06:消息流程分析之快速查找過程

上一篇文章,咱們探索了,當實例對象調用實例方法時,方法insertsel和imp插入到cache的過程。如今咱們再來看看,當調用方法時,是如何從cache中將其取出來的,即sel-imp的快速查找緩存


objc_msgSend鋪墊

1. 源碼查看

在調用方法時,是怎麼跑到insert方法的呢?markdown

在源碼objc_cache.mm中搜索insert,除了能夠看到上篇文章中的insert方法以外,還找到了cache_fill方法: 是在cache_fill中,調用了insert的,再搜cache_fillless

也就是說在Cache writers緩存寫入中,會進行cache_fill操做,並且在緩存寫入前,會先進行Cache readers緩存讀取的過程,其中有objc_msgSend 和 cache_getImp函數


2. Clang

使用Clang編譯以下代碼:oop

@interface Person : NSObject
 - (void)sayHello; - (int)addNumber:(int)number;  @end  @implementation Person - (void)sayHello{  NSLog(@"Hello world"); } - (int)addNumber:(int)number{  return number+1; } @end  Person *p = [Person alloc]; [p sayHello]; int result = [p addNumber:2]; 複製代碼

在cpp文件中,咱們能夠看到編譯後的結果: 不管是調用類方法alloc,仍是實例方法sayHello,都會編譯成objc_msgSend(消息接收者,方法主體,方法參數..)性能

消息接收者的意義,在於經過消息接收者才能找到方法的尋根路徑ui


3. 拓展

#import <objc/message.h>
@interface Tercher : Person @end  @implementation Tercher @end   Person *p = [Person alloc]; [p sayHello]; objc_msgSend(p, sel_registerName("sayHello"));  Tercher *t = [Tercher alloc]; [t sayHello]; struct objc_super xsuper; xsuper.receiver = t; xsuper.super_class = [Person class]; objc_msgSendSuper(&xsuper, sel_registerName("sayHello")); 複製代碼

須要設置Build Settings->Enable Strict Checking of objc_msgSend Calls爲NO。spa


objc_msgSend快速查找

objc_msgSend

源碼中全局搜objc_msgSend,查找到objc-msg-arm64.s的彙編代碼:3d

objc_msgSend函數是全部OC方法調用的核心引擎,負責查找方法的實現,並執行。因調用頻率很是高,其內部實現對性能的影響大,因此使用匯編語言來編寫內部實現代碼。彙編的特色有速度快、參數不肯定性。rest

解析:

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

第一段代碼cmp(compared對比),咱們從註釋就能夠看到nil check判斷是否爲nil。其中p0是objc_msgSend的第一個參數消息接收者receiver,那麼這句代碼的含義就是:判斷接收者是否存在


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

SUPPORT_TAGGED_POINTERS判斷是否支持小對象類型,支持會b.le跳轉到LNilOrTagged,不然b.eq LReturnZero返回空。

當支持小對象類型時,仍會由cmp p0, #0的結果來決定是否繼續,消息接收者爲空則一樣調用LReturnZero

le = less equal 小於等於; eq = equal 等於


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

根據對象拿到isa存入p13寄存器中。


4.
GetClassFromIsa_p16 p13     // p16 = class 
複製代碼

在64位真機中,將$0(傳入的p13->isa)ISA_MASK掩碼進行與運算,能夠獲得class類信息,查找到類信息後,就能夠偏移到cache進行方法的查找,即CacheLookup NORMAL 快速查找

5.
LGetIsaDone: // 獲取isa完畢
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend
複製代碼

CacheLookup NORMAL

源碼:

.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  ldr p11, [x16, #CACHE] // p11 = mask|buckets  #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16  and p10, p11, #0x0000ffffffffffff // p10 = buckets  and p12, p1, p11, LSR #48 // x12 = _cmd & mask #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    add p12, p10, p12, LSL #(1+PTRSHIFT)  // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))   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  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   // 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  .endmacro  複製代碼

1.
 // p1 = SEL, p16 = isa
 ldr p11, [x16, #CACHE]    // p11 = mask|buckets
複製代碼

其中#CACHE == 2*8 = 16爲: 由類結構可知,將isa位移16個字節,能夠獲得cache,即最終結果p11=cache。可是註釋爲何是p11 = mask|buckets? 緣由在於:在64位系統下爲了節省內存讀取方便,mask和buckets存在了一塊兒,cache的結構:


2.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
複製代碼

p11(mask|buckets)0x0000ffffffffffff進行與運算,會將其高16位進行抹零,獲得的結果就是buckets,存入p10

and p12, p1, p11, LSR #48分爲兩段,首先計算p11, LSR #48,將p11進行邏輯右移48位,便可獲得cache中的mask。而後將p1與運算mask的結果存在p12中。其中p1爲sel(_cmd)。看CacheLookup源碼中最開頭的註釋裏。 最終與運算的結果p12,就是方法存在buckets下標

由於在上篇文章,insert方法中要插入的位置,就是利用sel & mask計算獲得的下標.


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

這裏也分爲兩段看待:

  • p12, LSL #(1+PTRSHIFT)。全局搜索PTRSHIFT

64位真機下,PTRSHIFT = 3,那麼第一段代碼的含義就是將方法的下標進行邏輯左移4位。左移4位也等同於2^4

0000 0001 << 4  = 0001 0000 = 16 = 2^4
複製代碼

因此上段彙編代碼的含義就是,方法的下標 * 2^4。獲得的結果存入p10

  • add p12, p10

p12保存的buckets的首地址,這段彙編就是從首地址,偏移方法下標 * 2^4個字節,獲得要查找的方法bucket_t

爲何下標乘上16個字節?緣由在於bucket_t中保存的是sel和imp,都爲8個字節,一個bucket_t就是16字節,因此下標乘每一個bucket_t的大小,就能夠找到,下標所指的bucket_t。bucket_t等同於彙編中的bucket,bucket_t是C語言中的結構體,bucket是彙編的。


4.
ldp	p17, p9, [x12]		// {imp, sel} = *bucket
複製代碼

以上經過獲得的方法所在的bucket,也就找到了其中的imp和sel,分別保存在p1七、p9


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

這段彙編代碼的含義,在註釋中也能夠很清楚的瞭解,經過對比查找到的bucket中的sel是否等於CacheLookup傳入的參數p1(_cmd),不相等b.ne 2f跳轉到第2步,相等則CacheHit $0 緩存命中,進行imp返回


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

查找到的sel不等於參數p1(_cmd)時,首先會判斷查找到的bucket是否等於buckets,便是否是buckets的開頭,相等會跳到3,不然ldp p17, p9, [x12, #-BUCKET_SIZE]!。即不相等時,會向前找bucket,再次跳轉到1進行loop循環。


7.
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等於buckets,即等於開頭第一個時:add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))

其中p11在最開始就知道p11 = mask|buckets,p11邏輯右移44位,也能夠認爲是p11中的mask左移了4位,即註釋中 (mask << 1+PTRSHIFT) == mask * 2^4

在上篇文章中,mask的值等於capacity-1,即buckets中全部的結構體個數減一。

因此這句彙編的含義就是:當查找的bucket等於buckets中第一個時,會偏移到最後一個bucket,再次進行比較


快速查找的過程總結:



推薦參考

深刻解構objc_msgSend函數的實現

objc_msgSend流程分析之快速查找

相關文章
相關標籤/搜索