衆所周知,OC 是一門動態語言,由於 runtime
的存在而變得強大,而在代碼中調用方法就是給對象發送消息也是由於 runtime
的存在,調用方法就是調用 objc_msgSend
這個函數,那在底層又是怎麼樣的呢?彙編又是怎麼一步步調用的呢?這篇文章會將經過彙編來分析 objc_msgSend
都作了啥。緩存
clang
編譯咱們在 main
中寫兩個方法,而後對其進行 clang
一下,在 .cpp
文件最後能發現以下代碼。bash
objc_msgSend
函數,第一個參數是消息接受者,第二個參數是方法名稱(第二個參數能夠替換成咱們很熟悉的
@selector
)。
簡單來講給
OC
對象發送消息就是找函數實現的過程,OC
方法底層就是經過sel
去找imp
的過程,而C
函數名就是函數指針,經過函數指針就能夠直接找到函數實現。函數
objc_msgSend
objc_msgSend
源碼在 main
函數中給方法打斷點進行彙編分析,而後跳到 objc_msgSend
函數裏面,如右圖,就能得知 objc_msgSend
源碼須要去 libobjc.A.dylib
庫中找,操做以下圖。oop
疑問點:爲何
objc_msgSend
是一段彙編,而不是C
或者是C++
更加直接呢?post我的觀點:ui
1.彙編更加容易被機器識別 2.參數未知,對於靜態的
C
或者是C++
來講是很難接受的spa
objc_msgSend
彙編分析咱們來到 objc.750版本
源碼中,經過全局搜索 objc_msgSend
,找到在 objc-msg-arm64.s
的彙編代碼。3d
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, // 判斷當前 p0 寄存器是否爲空,當前 p0 存的是 objc_object 對象地址
// 處理對象是 tagged pointer 或 nil 的狀況
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged
#else
b.eq LReturnZero
#endif
// 爲正常的消息發送流程,就會走以下代碼
ldr p13, [x0] // p13 = isa,把 x0 指向內存的前 64 位放到 p13(便是 objc_object 的 isa 成員變量)
GetClassFromIsa_p16 p13 // p16 = class,是一個宏,取面具,isa & ISA_MASK,獲得當前類
LGetIsaDone:
CacheLookup NORMAL // 查找緩存
複製代碼
此時對 isa
處理已經完成,已經找到當前類,接下來就是去緩存裏面找方法,若是有直接返回對應的 imp
,接下來咱們經過 command + F
搜索 CacheLookup
,發現 CacheLookup
的參數分爲三種,NORMAL
(正常的去查找) 、 GETIMP
(直接返回 IMP) 和 LOOKUP
(主動的慢速去查找)。指針
.macro CacheLookup
// p1 = SEL, p16 = isa
// x16表明 class,#CACHE 是一個宏定義 #define CACHE (2 * __SIZEOF_POINTER__),表明16個字節
// class 平移 CACHE(也就是16個字節)獲得 cache_t,而後將 cache_t裏面的 buckets 和 occupied|mask 賦值給 p10和p11
// 爲何 occupied|mask 兩個值給了一個寄存器呢?由於 occupied|mask 都是隻佔4字節,而一個寄存器是8字節,這樣賦值給一個寄存器節省內存
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask,iOS 爲小端模式,w11只取前面四個字節,爲 mask
#endif
and w12, w1, w11 // x12 = _cmd & mask,獲得當前方法 hash 表的下標
add p12, p10, p12, LSL #(1+PTRSHIFT) // LSL 左移
// p10(buckets) 平移 p12 左移 #(1+PTRSHIFT) 以後的值
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket,經過 bucket 取出方法的 imp 和 sel
// 判斷 bucket 的 sel 和 _cmd 是否相同,p9爲sel
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 若是不一樣,走第二步,也就是 CheckMiss
b.ne 2f // scan more
// 若是相同,就會命中緩存,直接返回 imp,當前的 imp 存在 $0 裏面
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
// 比較 bucket
cmp p12, p10 // wrap if bucket == buckets
// 相同,則會走第三步,將上面流程再走一次,從新查找一次,若是仍是查找不到就會 JumpMiss
b.eq 3f
// 不一樣,就會 *--bucket 循環
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
複製代碼
上述分析,感受 CheckMiss
裏面應該有咱們想找的代碼,接下來就去分析一下 CheckMiss
。rest
.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
複製代碼
當前參數爲 NORMAL
,因此若是沒找到就會走 __objc_msgSend_uncached
。
根據上述分析得出 CacheLookup
包含讀取方法緩存的核心邏輯,主要產生兩種結果:若緩存命中,返回 IMP
或調用 IMP
;若緩存未命中,調用 __objc_msgSend_uncached
(找到IMP會調用) 或 __objc_msgLookup_uncached
(找到IMP不會調用) 方法。
當 CheckMiss
來到 __objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
複製代碼
MethodTableLookup
後面是比較複雜的邏輯,下面會分析,TailCallFunctionPointer x17
若找到了 IMP
會放到 x17
寄存器中,而後把 x17
的值傳遞給 TailCallFunctionPointer
宏調用方法。
MethodTableLookup
.macro MethodTableLookup
// push frame
SignLR
// 後面要跳轉函數,意味着lr的變化,因此開闢棧空間後須要把以前的fp/lr值存儲到棧上便於復位狀態
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
// 對參數進行處理,方便後面進行調用
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
// bl 是跳轉,跳轉到 __class_lookupMethodAndLoadCache3 方法
bl __class_lookupMethodAndLoadCache3
// IMP in x0
mov x17, x0
// restore registers and return
...
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
複製代碼
當咱們進行全局搜索 __class_lookupMethodAndLoadCache3
方法的時候卻怎麼也搜索不到,__
表明着彙編函數,而 __class_lookupMethodAndLoadCache3
是 C
函數,咱們去掉一個 _
進行全局搜索,咱們就來到了 lookUpImpOrForward
消息查找流程。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼
ENTRY _objc_msgSend
(id self,sel _cmd)
判斷處理LNilOrTagged
判斷處理GetClassFromIsa_p16
的 isa
的指針處理,isa & ISA_MASK
獲得當前的類CacheLookup
查找緩存cache_t
處理 bucket
以及內存哈希的處理__objc_msgSend_uncached
告訴找不到緩存的 imp
MethodTableLookup
跳轉到方法查找的流程bl __class_lookupMethodAndLoadCache3
方法查找的流程開始