Objective-C 裏每一個對象都會指向一個類,每一個類都會有一個方法列表,方法列表裏的每一個方法都是由 selector、函數指針imp 和 metadata 組成的。objc_msgSend的工做就是傳入對象和selector,查找相應方法的函數指針,而後跳到函數指針所指向的位置。html
objc_msgSend是用匯編寫的緣由有兩個:緩存
消息發送的代碼能夠被分爲兩部分:objc_msgSend中有一個快速路徑,是用匯編寫的,還有一個慢速的路徑,是用C實現的。彙編部分主要實現的是在緩存中查找方法,而且若是找到的話就跳轉過去的一個過程。若是在緩存中沒有找到方法的實現,就會調用C的代碼來處理後續的事情。bash
分析objc_msgSend的流程:多線程
根據上述流程分析objc_msgSen的彙編。架構
如今,蘋果公司已經開源了 Objective-C 的運行時代碼。你能夠在蘋果公司的開源網站,找到 objc_msgSend 的源碼。app
ARM64架構下有31個通用寄存器,每一個都是64位寬的。他們被標記爲x0~x30。一樣也有可能使用w0到w30來訪問寄存器的低32位。寄存器x0~x7被用於函數入參的前8個參數。這就表示objc_msgSend收到的self參數是保存在x0中,selector _cmd參數在x1裏。函數
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
複製代碼
判斷存儲在p0中的self是否爲空。若是小於0,跳轉到 LNilOrTagged, 執行tagged_pointers狀況,若是等於0,跳轉到 LReturnZero ,執行發送消息給nil的狀況。(在ARM64上 經過設置指針的高位來指明是tagged pointer。(x86-64上是設置低位)。若是高位被設置了1,且被做爲一個帶符號的整型解析的時候,那麼值就是負數。通常狀況下self是正常的,不會進入這些分支。)oop
ldr p13, [x0] // p13 = isa
複製代碼
加載x0所指向的內存,實則是加載self的isa指針。一個對象的第一個指針就是isa指針。p13寄存器存儲了isa。性能
GetClassFromIsa_p16 p13 // p16 = class
複製代碼
執行宏 GetClassFromIsa_p16網站
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // 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__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
複製代碼
判斷 SUPPORT_INDEXED_ISA ,表示isa_t中存放的信息是Class的地址,仍是一個索引(根據索引能夠在類信息表中查找該類的結構地址)。iOS設備SUPPORT_INDEXED_ISA 爲0,而後判斷是否爲64位,執行
and p16, $0, #ISA_MASK
複製代碼
ARM64可使用非指針的isa。一般isa指針指向的是對象的類,可是非指針的isa利用了備用的bit位,填充了一些其餘的信息。這條彙編指令執行了一個邏輯與運算,掩蓋掉了全部額外的位,把實際的指向類的指針保存在p16寄存器中。
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
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
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// 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
3: // double wrap
JumpMiss $0
.endmacro
複製代碼
分析上面的宏
#define CACHE (2 * __SIZEOF_POINTER__)
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
複製代碼
從x16寄存器編譯16個字節,取到的值保存到p10和p11中。方法緩存的結構以下:
typedef uint32_t mask_t;
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
複製代碼
p10存儲了buckets的值,p11的高32位保存了_occupied,低32位保存了_mask。
_occupied指定了哈希表中包含了多少條目,在objc_msgSend中不起什麼做用。_mask很重要:它描述了哈希表的尺寸,方便用於與運算的掩碼。它的值老是一個2的冪減一,用二進制的方法描述看起來就像是000000001111111,末尾是可變數量的1。經過這個值能夠知道selector的查找索引,並在查找表的時候包裹着結尾。
and w12, w1, w11 // x12 = _cmd & mask
複製代碼
x1中包含_cmd,因此w1包含了_cmd的低32位。w11包含了上面提到的_mask。這條指令將這兩個值作與運算並將結果放到w12中。結果至關因而計算_cmd & mask,可是避免了開銷很大的模運算。這一步計算出了傳入的selector的起始哈希表的索引。
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
複製代碼
這條指令經過索引表的指針向左位移,再加上buckets,獲得第一個查找的bucket的地址,也就是imp。
ldp p17, p9, [x12] // {imp, sel} = *bucket
複製代碼
把x12保存的bucket的地址,每一個bucket包含一個selector和imp。p17包含了當前的imp,p9包含了當前的selector。
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
複製代碼
對p1中的cmd和p9中的selector比較。若是相等,CacheHit將imp返回。接下去就是執行目標方法的代碼了,objc_msgSend的快速路徑到此已經結束了。全部參數寄存器不會受到干擾,原封不動的傳給目標方法,就好像直接調用了目標方法同樣。若是不相等,跳轉到2f位置,處理不相等的邏輯,執行CheckMiss。
來看CheckMiss的宏定義
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製代碼
因爲傳入的是nomal模式,會進入__objc_msgSend_uncached
cbz p9, __objc_msgSend_uncached
複製代碼
p9包含了從bucket加載的selector,這條指令是p9和0做比較,若是等於0跳轉到__objc_msgSend_uncached,這就說明這是個空的bucket,意味着目標方法不在緩存中,這時候會進入C語言方法__objc_msgSend_uncached,執行詳細的查找流程。若是不爲0就說明bucket不是空,只是沒有找到,則繼續查找。
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
複製代碼
p12中存儲的是當前的bucket地址,p10中存儲的是buckets哈希表的首地址,比較若是匹配,跳轉到3f處。 若是不匹配會繼續執行
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
複製代碼
再一次從緩存的bucket中加載。此次他從偏移量爲BUCKET_SIZE的地方加載當前緩存bucket的地址。地址引用末尾的感嘆號是一個有趣的特性。這指定一個寄存器進行回寫,意思就是寄存器會更新爲計算後的值。這條指令有效的執行了x12 -= 16來加載新的bucket,並使x12指向這個新的bucket。
如今已經加載了一個新的bucket,因此接下去的執行就要回到以前的檢查當前bucket是否匹配的代碼。這條指令表明回到1b,使用新的值再執行一次全部代碼。若是仍然沒有找到匹配的bucket,這些代碼會持續執行,直到找到匹配的,或者空的bucket,或者命中表的開頭。
3f處匹配的邏輯指令:
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// 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
複製代碼
x12中包含了當前的bucket指針,w11表示的是mask,表的大小。將mask左移1+PTRSHIFT位,加上buckets的首地址。獲得的結果指向表的末尾。
把獲得的新的bucket存儲到p17,p9。
cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
複製代碼
這段代碼仍是去檢測bucket是否匹配,而且跳轉返回imp。咱們能夠看到有兩次123的流程,第二次123就是爲了防止兩種狀況:
第一種,多線程調用的時候給的一次容錯機會。
第二種是 爲了在遇到內存被破壞或者無效對象時,防止陷入無限循環而榨乾性能。舉個例子,堆損壞可以在緩存中塞滿非0的數據,或者設置緩存的掩碼爲0,緩存不命中就會一直循環執行緩存掃描。額外的檢查能夠中止循環,將問題轉變爲崩潰日誌。
參考資料: