閱讀此文須要對於objc_object、objc_class以及結構體內部cache_t有必定的瞭解。c++
環境:xcode 11.5
算法
源碼:objc4-781
數組
當咱們對oc
代碼進行clang
處理轉換爲c++
代碼以後,咱們會發現,oc
的方法調用會被進行以下轉換:xcode
源碼:
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];
轉換以後:
LGTeacher *teacher = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("sayHello"));
複製代碼
最終都會調用消息發送objc_msgSend
方法,定義以下:緩存
objc_msgSend(id receiver, SEL op, ...)
第一個參數id receiver爲消息的接收者
第二個參數SEL op爲消息的名稱SEL
...爲可變參數
複製代碼
objc_msgSend
是全部OC
方法調用的核心,調用效率是至關的高,所以處於性能考慮,這個函數的內部代碼是用匯編來實現。在arm64
下的具體核心實現以下所示:markdown
以以下代碼爲例:
// objc_msgSend(id receiver, SEL op, ...)
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//判斷receiver是否爲空
cmp p0, #0 // nil check and tagged pointer check
//處理異常邏輯
#if SUPPORT_TAGGED_POINTERS
//le less than or equal
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 讀取x0寄存器即receiver的首地址所指向的內容存入p13,receiver是objc_object結構體指針,所以首地址對應的內存區域存放的是isa
ldr p13, [x0] // p13 = isa
// 從isa中獲取Class,存入p16
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// 查找緩存,調用緩存或者進行快速查找objc_msgSend_uncached
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
複製代碼
objc_msgSend
的主要流程爲:less
receiver
中查找到receiver
對應的isa
指針進而找到對應的類。接下來看一下具體的幾個方法。函數
該方法主要用來從isa指針來獲得對應的類。oop
.macro GetClassFromIsa_p16 /* src */
...
#elif __LP64__
//只關注arm64
// $0爲上述流程中傳入的參數p13,
// p16 = isa & #ISA_MASK, ISA_MASK爲isa.h中聲明的掩碼,經過這一步操做,能夠獲得isa_t結構體中對應的cls
and p16, $0, #ISA_MASK
...
複製代碼
.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:
//#define CACHE (2 * __SIZEOF_POINTER__)
// p1 = SEL, p16 = isa
// 讀取x16+16個字節長度爲地址的內容,存入p11中,根據objc_class的結構,cache_t和isa的偏移量就是16個字節,分別是isa8個字節以及superClassb8個字節
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 只關注arm64
// 0x0000ffffffffffff爲arm64下的buckets掩碼, 按位與以後獲得buckets指針
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// p1爲msgSend的第二個參數_cmd
// p11=cache_t右移48位獲得cache_t中的mask
// _cmd & mask和方法緩存中的哈希算法一致,以此來進行快速緩存方法查找
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
// PTRSHIFT 3
// p12即hash值左移4位至關於p12*16,獲得對應的bucket指針相對於bucket數組的偏移量,進而獲得當前hash值對應的bucket指針放入p12中
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 讀取x12內容爲地址的值依次放入p17和p9中,arm64中bucket_t結構體佈局爲{imp,sel}
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 比較p9=sel和p1=cmd
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 不相等,向前跳轉至2
b.ne 2f // scan more
// 相等,執行命中的指令
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
// 根據sel的值判斷是否查找結束
CheckMiss $0 // miss if bucket->sel == 0
// 若是sel不爲0,比較當前命中的bucket是否和buckets數組的第一個bucket相等
cmp p12, p10 // wrap if bucket == buckets
// 相等,跳轉至3
b.eq 3f
// 不相等,以x12即當前bucket地址往前移動一個bucket大小爲地址讀取其中的值,並賦值爲p17和p9,同時x12=x12-BUCKET_SIZE
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
// 返回1進行循環
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 接上步驟,若是p12和buckets數組第一個相等,須要移動到buckets數組的最後一位
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
// 若是從buckets數組最後一個開始往前尋找又找到了buckets數組第一個bucket,表明查找結束,並無找到當前的sel,執行JumpMiss操做。
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
複製代碼
這一步主要操做是在類objc_class中的緩存cache_t中快速查找是否有緩存。主要步驟以下:佈局
_cmd&mask
獲得hash
值hash
值獲得對應的bucket->sel與_cmd
做對比CheckMiss
,若是bucket->sel==0
表明_cmd
對應的方法並無緩存過,或者在緩存擴容的時候被清空了。bucket->sel!=_cmd
,則往前一個bucket
繼續進行對比,若是到達buckets數組
第一個則從數組的最後一個bucket
開始對比。bucket
開始往前查找又回到了第一個,則表明出現了問題,緩存已經滿了,執行JumpMiss
。bucket->sel==_cmd
,命中緩存,執行CacheHit
。// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
// NORMAL狀況下回執行調用imp的流程
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, 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, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
複製代碼
.macro CheckMiss
// miss if bucket->sel == 0
// cbz p9 判斷p9是否爲0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
// NORMAL狀況下回執行調用objc_msgSend_uncached的流程
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製代碼
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
// NORMAL狀況下回執行調用objc_msgSend_uncached的流程
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製代碼
其中CheckMiss
和JumpMiss
都是快速查找方法沒有找到的狀況。
CheckMiss
是找到了一個空的位置,由於查找的算法和緩存的算法是一致的,意味着若是方法被緩存過,必然是放在當前的空位置。既然當前位置爲空,說明該方法並無被緩存過。那麼就要執行消息的慢速查找了。JumpMiss
表明整個緩存列表都找過了尚未找到_cmd
對應的方法,這種狀況我有些疑問,cache_t是會擴容的,是否是意味着在正常的狀況下必定會有空位,那爲何還能遇到列表滿的狀況?有大佬的話能夠解答一下。以上是objc_msgSend發送消息時在彙編層面的快速查找流程,進入__objc_msgLookup_uncached
就屬於慢速查找了,後面再議。
快速查找主要流程圖以下: