上一篇文章,咱們探索了,當實例對象調用實例方法時,方法insert
將sel和imp
插入到cache
的過程。如今咱們再來看看,當調用方法時,是如何從cache
中將其取出來的,即sel-imp的快速查找
。緩存
在調用方法時,是怎麼跑到insert
方法的呢?markdown
在源碼objc_cache.mm
中搜索insert,除了能夠看到上篇文章中的insert方法以外,還找到了cache_fill
方法: 是在
cache_fill
中,調用了insert
的,再搜cache_fill
:less
也就是說在
Cache writers
緩存寫入中,會進行cache_fill
操做,並且在緩存寫入前,會先進行Cache readers
緩存讀取的過程,其中有objc_msgSend 和 cache_getImp
。函數
使用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
#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-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
複製代碼
源碼:
.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,再次進行比較
快速查找的過程總結: