前言
該系列我們來探究一下OC
的消息發(fā)送和轉(zhuǎn)發(fā)機(jī)制旺韭,本文我們就來對(duì)objc_msgSend
做一下初步探索,明白方法調(diào)用是如何快速尋找到方法的污筷。以后我們會(huì)探索到慢速尋找方法以及找不到方法是如何進(jìn)行消息轉(zhuǎn)發(fā)的。
runtime
簡(jiǎn)介
要探索objc_msgSend
,我們首先要了解runtime
档泽。runtime
是C
袍啡、C++
胖秒、匯編混合寫成的一套為Objective-C
提供運(yùn)行時(shí)功能的API
。也是因?yàn)?code>runtime梨撞,Object-C
才被成為動(dòng)態(tài)語言燥滑。
runtime
的版本
runtime
的版本分為兩個(gè)版本modern
和legacy
(官方文檔)渐北,我們現(xiàn)在使用的Objective-C 2.0
版本就是modern
版本,只能適用于iOS
和64 bit OS X 10.5
版本及更高版本铭拧;legacy
則適用于其他版本和32 bit OS X
赃蛛。modern
和legacy
最大的區(qū)別就是如果更改類中實(shí)例變量的布局,legacy
需要重新編譯他的子類搀菩,modern
版本則不需要呕臂。
runtime
的使用
runtime
的使用大致可分為三種使用方法。
-
Objective-C
code:@selector()
等; -
NSObject
的方法:NSSelectorFromString()
等; -
runtime
的api
:sel_registerName()
等肪跋;
編譯時(shí)和運(yùn)行時(shí)
編譯時(shí):即編譯器對(duì)語言的編譯階段歧蒋,編譯時(shí)只是對(duì)語言進(jìn)行最基本的檢查報(bào)錯(cuò),包括詞法分析、語法分析等等疏尿,將程序代碼翻譯成計(jì)算機(jī)能夠識(shí)別的語言(例如匯編等)瘟芝,編譯通過并不意味著程序就可以成功運(yùn)行。
運(yùn)行時(shí):即程序通過了編譯這一關(guān)之后編譯好的代碼被裝載到內(nèi)存中跑起來的階段褥琐,這個(gè)時(shí)候會(huì)具體對(duì)類型進(jìn)行檢查锌俱,而不僅僅是對(duì)代碼的簡(jiǎn)單掃描分析,此時(shí)若出錯(cuò)程序會(huì)崩潰敌呈。這個(gè)階段也是runtime
起作用的階段贸宏。
objc_msgSend
探索
一、clang
生成cpp
文件
創(chuàng)建工程磕洪,在main.m
寫入以下代碼:
void run(){
NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
//創(chuàng)建LGPerson類和對(duì)象方法sayNB
LGPerson *person = [LGPerson alloc];
[person sayNB];
run();
}
return 0;
}
打開終端進(jìn)入main.m
文件目錄下吭练,執(zhí)行以下命令:
clang -rewrite-objc main.m -o main.cpp
在此文件夾下會(huì)生成一個(gè)main.cpp
文件,打開文件滑動(dòng)到底部可以看到如下代碼:
void run(){
NSLog((NSString *)&__NSConstantStringImpl__var_folders_85_h8yymn657hq3vfgnz_xwbtjc0000gp_T_main_26fe1b_mi_0,__func__);
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
run();
}
return 0;
}
從代碼中可以看出析显,調(diào)用alloc
和sayNB
兩個(gè)方法被轉(zhuǎn)換成了objc_msgSend
發(fā)送消息((void (*)(id, SEL))(void *)
是類型強(qiáng)轉(zhuǎn))鲫咽,而我寫的一個(gè)run()
函數(shù)則是直接調(diào)用,不是通過objc_msgSend
進(jìn)行消息發(fā)送谷异,由此可以看出只有Objective-C
的方法是通過runtime
轉(zhuǎn)換為消息發(fā)送的分尸。
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
objc_msgSend
的兩個(gè)參數(shù)id
和sel
代表消息接收者和方法唯一標(biāo)識(shí)。
二歹嘹、斷點(diǎn)看匯編
在sayNB
處打斷點(diǎn)箩绍,如圖:
進(jìn)入斷點(diǎn),然后菜單 Debug -> Debug Workflow -> Always Show Disassembly
尺上,顯示匯編如下:
可以看到objc_msgSend
,然后按著control
+↓
進(jìn)入objc_msgSend
詳情怎抛,如下:
可已看出objc_msgSend
是在libobjc
里邊,接下來我們?nèi)フ以创a看看objc_msgSend
是如何快速進(jìn)行方法查找的唉窃。
三耙饰、objc_msgSend
匯編源碼
objc_msgSend
源碼是用匯編寫的,全局搜索objc_msgSend
找到匯編(文件表示上為s
)arm64
文件纹份,ENTRY _objc_msgSend
是開始如下:
objc_msgSend
匯編源碼如下:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
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
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
可以看出先進(jìn)行了nil
和tagged pointer
的檢測(cè)苟跪,SUPPORT_TAGGED_POINTERS
在arm64
下為1廷痘,ldr p13, [x0]
把在[x0]
位置的isa
存入p13
中,GetClassFromIsa_p16 p13
通過isa
獲取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
isa指針詳解文章中SUPPORT_INDEXED_ISA
在iOS設(shè)備上是0件已,那么進(jìn)入and p16, $0, #ISA_MASK
中笋额,也即是通過掩碼ISA_MASK
和isa
獲取類信息。
接下來全局搜索CacheLookup
篷扩,找到帶有.macro
的宏定義兄猩,是CacheLookup
詳情。如下:
.macro CacheLookup
// p1 = SEL, p16 = class
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
參考類的結(jié)構(gòu)分析鉴未,分析上方匯編代碼:
注意:
p
枢冤、w
、x
的區(qū)別
p16
代表指針铜秆;w16
代表32位下的值淹真,4字節(jié);x16
代表64位下的值连茧,8字節(jié)核蘸;
ldp p10, p11, [x16, #CACHE]
:全局搜索define #CACHE
會(huì)發(fā)現(xiàn)#CACHE
是16,通過GetClassFromIsa_p16
可以知道x16
代表class
啸驯,對(duì)象結(jié)構(gòu)里內(nèi)存平移16位(isa
和superclass
)可以得到cache
客扎,cache
又包含了_buckets
、_mask
和_occupied
坯汤。這句匯編的意思就是把_buckets
存入到p10
,把_mask
和_occupied
存入到p11
搀愧,又因?yàn)槭切《四J剑?code>p11 = occupied|mask惰聂。and w12, w1, w11
:這里用w
是因?yàn)榇?字節(jié)只取4字節(jié),即w11=mask
咱筛、w1
是sel
轉(zhuǎn)換之后的key
搓幌,w12
存儲(chǔ)的是key&mask
即方法在哈希表的索引值。add p12, p10, p12, LSL #(1+PTRSHIFT)
:p10
是buckets
的首地址迅箩,而bucket_t
結(jié)構(gòu)體占用16字節(jié)溉愁,所以buckets
的首地址加上索引向左偏移(1+PTRSHIFT)
字節(jié)得到的值就是函數(shù)方法在緩存中的地址。因此p12
就是函數(shù)方法對(duì)應(yīng)的bucket
地址饲趋。ldp p17, p9, [x12]
:將bucket
存放在p17
和p9
中拐揭,p17
裝imp
,p9
里裝sel
奕塑。1: cmp p9, p1
:比較取出來的sel
和p1
是否相等堂污,b.ne 2f
不相等進(jìn)入2:CheckMiss $0
緩存未命中;相等則是CacheHit $0
緩存命中龄砰。CacheHit
詳情如下:
// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
AuthAndResignAsIMP x0, x12 // authenticate imp and re-sign as IMP
ret // return IMP
.elseif $0 == LOOKUP
AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
CacheHit
就是找到了imp
盟猖,那么直接調(diào)用TailCallCachedImp
就完成了查找讨衣。
-
cmp p12, p10
:比較p12
和p10
是否相等,相等的話說明進(jìn)入3f:add p12, p12, w11, UXTW #(1+PTRSHIFT)
式镐,索引值即為mask
反镇;不相等則重新賦值p9
,循環(huán)進(jìn)入1f
娘汞。下方de就是進(jìn)入到循環(huán)查找imp
的循環(huán)中了歹茶。 -
JumpMiss $0
:跳轉(zhuǎn)到JumpMiss
。如下:
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
進(jìn)入NORMAL
判斷中价说,調(diào)用__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
然后調(diào)用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
最后是調(diào)用了__class_lookupMethodAndLoadCache3
方法鳖目,bl
是跳轉(zhuǎn)方法扮叨,該方法還帶有雙下劃線,并且搜不到方法的具體實(shí)現(xiàn)领迈,可以得出該方法不再是匯編方法彻磁,應(yīng)該是跳轉(zhuǎn)到了C
或者C++
的方法。
到此我們就把objc_msgSend
匯編快速查找方法的探索完了狸捅,那為什么要用匯編語言查找方法呢衷蜓?大概是有兩個(gè)原因:
1、這個(gè)過程需要的是速度尘喝,匯編更容易被計(jì)算機(jī)識(shí)別磁浇,速度更快。
2朽褪、因?yàn)榉椒ǘ紩?huì)有傳參和返回參數(shù)置吓,而且是不確定的,相對(duì)于C
或者C++
是很難實(shí)現(xiàn)這些的缔赠,但是匯編是可以的衍锚。
總結(jié)
1、Objective-C
調(diào)用方法是一個(gè)通過objc_msgSend
發(fā)送消息進(jìn)行查找方法的實(shí)現(xiàn)imp
的嗤堰。
2戴质、objc_msgSend
查找方法首先是匯編語言查找,這是一個(gè)快速的過程踢匣。還有一個(gè)是慢速查找的過程告匠。