iOS 底層探索系列
我們?cè)谇懊嫣剿髁藢?duì)象和類的底層原理,接下來(lái)我們要探索一下方法的本質(zhì),而在探索之前包晰,我們先簡(jiǎn)單過(guò)一遍 Runtime
的知識(shí)點(diǎn)整吆,如果讀者對(duì)這塊內(nèi)容已經(jīng)很熟悉了的話可以直接跳過(guò)第一章。
PS: 由于筆者對(duì)匯編暫時(shí)還是摸索的階段猫妙,關(guān)于匯編源碼的部分如有錯(cuò)誤入宦,歡迎指正。
一倦畅、Runtime
簡(jiǎn)介
眾所周知,Objective-C
是一門動(dòng)態(tài)語(yǔ)言绣的,而承載整個(gè) OC
動(dòng)態(tài)特性的就是 Runtime
叠赐。關(guān)于 Runtime
更多內(nèi)容可以直接進(jìn)入官網(wǎng)文檔查看。
Runtime
是以 C
/C++
和匯編編寫而成的屡江,為什么不用 OC
呢芭概,這是因?yàn)閷?duì)我們編譯器來(lái)說(shuō),OC
屬于更高級(jí)的語(yǔ)言惩嘉,相比于 C
和 C++
以及匯編罢洲,執(zhí)行效率更慢,而在運(yùn)行時(shí)系統(tǒng)需要盡可能快的執(zhí)行效率文黎。
1.1 Runtime
的前世今生
Runtime
分為兩個(gè)版本惹苗,legacy
和 modern
,分別對(duì)標(biāo) OC 1.0
和 OC 2.0
耸峭。我們通常只需要專注于 modern
版本即可桩蓉,在 libObjc
源碼中體現(xiàn)在 new
后綴的文件上。
1.2 Runtime
三種交互方式
我們與 Runtime
打交道有三種方式:
- 直接在
OC
層進(jìn)行交互:比如@selector
-
NSObject
的方法:NSSelectorFromName
-
Runtime
的函數(shù):sel_registerName
二劳闹、方法的本質(zhì)探索
2.1 方法初探
我們可以看到院究,通過(guò) clang
重寫之后洽瞬,sayNB
在底層其實(shí)是一個(gè)消息的發(fā)送。
我們把右側(cè)的發(fā)送消息的代碼簡(jiǎn)化一下:
LGPerson *person = objc_msgSend((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
objc_msgSend((id)person, sel_registerName("sayNB"));
由此可見(jiàn)业汰,真正發(fā)送消息的地方是 objc_msgSend
伙窃,這個(gè)方法有兩個(gè)參數(shù),一個(gè)是消息的接受者為 id
類型蔬胯,第二個(gè)個(gè)是方法編號(hào) sel
对供。
作為對(duì)比,run
方法就直接執(zhí)行了氛濒,并沒(méi)有通過(guò) objc_msgSend
進(jìn)行消息發(fā)送:
2.2 方法發(fā)送的幾種情況
LGStudent *s = [LGStudent new];
[s sayCode];
objc_msgSend(s, sel_registerName("sayCode"));
上述代碼表示的是向?qū)ο?s
發(fā)送 sayCode
消息产场。
id cls = [LGStudent class];
void *pointA = &cls;
[(__bridge id)pointA sayNB];
objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));
上述代碼表示向 LGStudent
這個(gè)類發(fā)送 sayNB
消息。
// 向父類發(fā)消息(對(duì)象方法)
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));
上述代碼表示向父類發(fā)送 sayHello
消息舞竿。
//向父類發(fā)消息(類方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元類
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
上述代碼表示向父類的類京景,也就是元類發(fā)送 sayNB
消息。
我們?cè)?OC
中使用 objc_msgSend
的時(shí)候骗奖,要注意需要將 Enbale Strict of Checking of objc_msgSend Calls
設(shè)置為 NO
确徙。這樣才不會(huì)報(bào)警告。
三执桌、探索 objc_msgSend
objc_msgSend
之所以采用匯編來(lái)實(shí)現(xiàn)鄙皇,是因?yàn)?/p>
- 匯編更容易能被機(jī)器識(shí)別
- 參數(shù)未知、類型未知對(duì)于
C
和C++
來(lái)說(shuō)不如匯編更得心應(yīng)手
3.1 消息查找機(jī)制
- 快速流程
- 慢速流程
3.2 定位 objc_msgSend
匯編源碼
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
判斷 p0
仰挣,也就是我們 objc_msgSend
的第一個(gè)參數(shù) id
消息的接收者是否為空伴逸。
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
讀取 x0
然后賦值到 p13
,這里 p13
拿到的是 isa
膘壶。為什么要拿 isa
呢错蝴,因?yàn)椴徽撌菍?duì)象方法還是類方法,我們都需要在類或者元類的緩存或方法列表中去查找颓芭,所以 isa
是必需的顷锰。
3.3 GetClassFromIsa_p16
通過(guò) GetClassFromIsa_p16
,將獲取到的 class
存在 p16
上面亡问。
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
這個(gè)方法的目的就是通過(guò)位移操作獲取 isa
的 shiftcls
然后進(jìn)行位運(yùn)算與操作得到真正的類信息官紫。
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
3.4 CacheLookup
獲取完 isa
之后,接下來(lái)就要進(jìn)行 CacheLookup
玛界,查找方法緩存万矾,我們?cè)賮?lái)到 CacheLookup
的源碼處:
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
.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
通過(guò)上述代碼可知 CacheLookup
有三種模式:NORMAL
,GETIMP
慎框, LOOKUP
ldp p10, p11, [x16, #CACHE]
-
CacheLookup
需要讀取上一步拿到的類的cache
緩存,而根據(jù)我們前面對(duì)類結(jié)構(gòu)的學(xué)習(xí)后添,這里顯然進(jìn)行 16 字節(jié)地址平移操作笨枯,然后把拿到的cache_t
中的buckets
和occupied
、mask
賦值給p10
,p11
。
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
- 這里是將
w1
和w11
進(jìn)行與操作馅精,其實(shí)本質(zhì)就是_cmd
&mask
严嗜。這一步和我們探索cache_t
時(shí)遇到的
是一模模一樣樣的道理。目的就是拿到下標(biāo)洲敢。然后經(jīng)過(guò)哈希運(yùn)算之后漫玄,得到了bucket
結(jié)構(gòu)體指針,然后將這個(gè)結(jié)構(gòu)體指針中的imp
压彭,sel
分別存在p17
睦优,p9
中。
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
- 接著我們將上一步獲取到的
sel
和我們要查找的sel
(在這里也就是所謂的_cmd
)進(jìn)行比較壮不,如果匹配了汗盘,就通過(guò)CacheHit
將imp
返回;如果沒(méi)有匹配询一,就走下一步流程隐孽。
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
沒(méi)有匹配上,我們需要接著進(jìn)行搜索健蕊。
3.5 CheckMiss
我們來(lái)到 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
這里由于我們是 NORMAL
模式菱阵,所以會(huì)來(lái)到 __objc_msgSend_uncached
:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
__objc_msgSend_uncached
中最核心的邏輯就是 MethodTableLookup
,意為查找方法列表缩功。
3.6 MethodTableLookup
我們?cè)賮?lái)到 MethodTableLookup
的定義:
.macro MethodTableLookup
// push frame
SignLR
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
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
我們觀察 MethodTableLookup
內(nèi)容之后會(huì)定位到 __class_lookupMethodAndLoadCache3
晴及。在 __class_lookupMethodAndLoadCache3
之前會(huì)做一些準(zhǔn)備工作,真正的方法查找流程核心邏輯是位于 __class_lookupMethodAndLoadCache3
里面的掂之。 但是我們?nèi)炙阉?__class_lookupMethodAndLoadCache3
會(huì)發(fā)現(xiàn)找不到抗俄,這是因?yàn)榇藭r(shí)我們會(huì)從匯編跳入到 C/C++
。所以去掉一個(gè)下劃線就能找到:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
四世舰、總結(jié)
- 方法的本質(zhì)就是消息發(fā)送动雹,消息發(fā)送是通過(guò)
objc_msgSend
以及其派生函數(shù)來(lái)實(shí)現(xiàn)的。 -
objc_msgSend
為了執(zhí)行效率以及 C/C++ 不能支持參數(shù)未知跟压,類型未知的代碼胰蝠,所以采用匯編來(lái)實(shí)現(xiàn)objc_msgSend
。 - 消息查找或者說(shuō)方法查找震蒋,會(huì)優(yōu)先去從類中查找緩存茸塞,找到了就返回,找不到就需要去類的方法列表中查找查剖。
- 由匯編過(guò)渡到 C/C++钾虐,在類的方法列表中查找失敗之后,會(huì)進(jìn)行轉(zhuǎn)發(fā)笋庄。核心邏輯位于
lookUpImpOrForward
效扫。
我們下一章將會(huì)從 lookUpImpOrForward
開(kāi)始探索倔监,探索底層的方法查找的具體流程到底是怎么樣的,敬請(qǐng)期待~