消息發(fā)送之快速轉(zhuǎn)發(fā)
在之前文章objc_class中的cache_t分析中,我們分析了,cache_t存儲(chǔ)方法的過程,有留下一個(gè)疑問:cache_t在存儲(chǔ)方法之前有讀取方法的,那它是怎么讀取方法的呢?
首先,讓我們先了解,什么是方法,方法的本質(zhì)是什么。顧名思義,類或者對(duì)象調(diào)用一個(gè)API,這個(gè)API就是方法,平時(shí)我們接觸太多了,有自己定義的方法,也有調(diào)用系統(tǒng)的方法,那么這個(gè)方法在底層到底是什么?在何時(shí)調(diào)用的。
一、方法的本質(zhì)
1.1 方法是何時(shí)調(diào)用
創(chuàng)建一個(gè)
student類,Teacher類,student繼承Teacher。在student里聲明sayStu方法,但沒有實(shí)現(xiàn)。> 編譯時(shí)和運(yùn)行時(shí)圖-
依次
編譯(command+B)、運(yùn)行(command+R),結(jié)果:編譯是成功的,運(yùn)行時(shí)是崩潰報(bào)錯(cuò)的>
方法沒實(shí)現(xiàn)@2x.png -
可以得知我們方法都是在運(yùn)行時(shí)執(zhí)行的。
補(bǔ)充:
編譯時(shí)和運(yùn)行時(shí)概念
編譯時(shí):編譯器把源代碼翻譯成機(jī)器可以識(shí)別的代碼過程。簡(jiǎn)單的說,是個(gè)翻譯的工作,檢查代碼里有沒有錯(cuò)寫的關(guān)鍵字、詞法分析、語法分析之類靜態(tài)類型檢查
運(yùn)行時(shí):簡(jiǎn)單得說,就是代碼跑起來,被裝載到內(nèi)存里面去(代碼保存在磁盤上沒有裝入到內(nèi)存之前是個(gè)死的東西,只有到內(nèi)存里才是活的),如果此時(shí)出錯(cuò),則程序會(huì)崩潰,是一個(gè)動(dòng)態(tài)階段。
OC的運(yùn)行時(shí)機(jī)制:一是將數(shù)據(jù)類型的確定由編譯時(shí),推遲到運(yùn)行時(shí)。OC的這種運(yùn)行時(shí)機(jī)制使對(duì)象的類型及對(duì)象的屬性和方法在運(yùn)行時(shí)才能確定;二是讓OC具備多態(tài)(不同對(duì)象以自己的方式響應(yīng)相同的消息)特性
Runtime的三種調(diào)用:oc代碼調(diào)用(如自定義方法)、framework調(diào)用(系統(tǒng)方法) 、RuntimeAPI(如class_getInstanceSize)調(diào)用

1.2 方法在底層是如何實(shí)現(xiàn)的
-
下面我們給
student加上sayStu的實(shí)現(xiàn),再clang一下main.m文件,看下底層方法是如何調(diào)用的clang(是一個(gè)由Apple主導(dǎo)編寫,基于LLVM的C/C++/OC的編譯器):clang -rewrite-objc main.m -o main.cpp
搜索sayStu,可以觀察到,其實(shí)是調(diào)用了objc_msgSend函數(shù),而且alloc方法也調(diào)用了objc_msgSend函數(shù)
方法的本質(zhì)@2x.png 調(diào)用格式:objc_msgSend: (消息接收者, 消息主體)
-
嘗試在
main文件里使用objc_msgSend調(diào)用方法
需要導(dǎo)入頭文件#import <objc/message.h>;
手動(dòng)關(guān)閉運(yùn)行時(shí)的編譯警告: buildSettings->Enable Strict Checking of objc_msgSend Calls->設(shè)置為No(對(duì)objc_msgSend調(diào)用的嚴(yán)格檢查關(guān)閉)
加入測(cè)試代碼objc_msgSend(p, sel_registerName("sayStu"));
打印結(jié)果
三種方式調(diào)用@2x.png我們發(fā)現(xiàn),是調(diào)用成功的,三種形式的調(diào)用方式都是一個(gè)效果。
由此我們可以得出結(jié)論:方法的本質(zhì)就是方法名和對(duì)應(yīng)的函數(shù)代碼
oc層面:是對(duì)象/類+方法名
底層層面:是objc_msgSend發(fā)送消息sel->通過sel(方法編號(hào))找到imp(函數(shù)指針地址) -> 找到函數(shù)內(nèi)容
上述流程我們唯一不清楚的是怎么通過sel找到imp的
下面就分析方法編號(hào)是如何綁定函數(shù)指針地址的
二、objc_msgSend解析
- 打開objc源碼,搜索
objc_msgsend,發(fā)現(xiàn)objc_msgsend是匯編語言寫的。原因是:一是整個(gè)程序方法很多,調(diào)用極其頻繁的,所以要求速度快。二是參數(shù)的不確定性,如果用C/C++,它們是靜態(tài)性的,速度會(huì)很慢。總體來說還是為了性能更好。 - 還有一個(gè)疑問是,
objc_msgSend為什么需要需要傳入?yún)?shù)消息接受者,這是因?yàn)橄⒔邮苷呃镉?code>isa,isa->類/元類->cache_t(方法緩存)->methodlist(存在bits里) -
imp的查找分為2個(gè)階段,快速查找(從緩存cache中查找,匯編語言編寫)和慢速查找(方法列表methodTable中,c/c++編寫),今天先分析快速查找 - 打開objc源碼,搜索
objc_msgsend,選擇arm64真機(jī)環(huán)境進(jìn)行探索(其他環(huán)境也是類似),找到objc_msgSend入口
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0 和空對(duì)比,即判斷接收者是否存在,其中p0是objc_msgSend的第一個(gè)參數(shù)-消息接收者receiver
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS//支持taggedpointer(小對(duì)象類型)的流程
b.le LNilOrTagged//判斷是否是小對(duì)象或者為nil,則跳轉(zhuǎn)到LNilOrTagged// (MSB tagged pointer looks negative)//
#else
b.eq LReturnZero//p0 等于 0 時(shí),直接返回 空
#endif
//p0即receiver 肯定存在的消息的流程
//根據(jù)對(duì)象內(nèi)存,首地址是isa,拿出isa ,即從x0寄存器指向的地址 取出 isa,存入 p13寄存器
ldr p13, [x0] // p13 = isa
//p16 = isa(p13) & ISA_MASK,得到class
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone://獲取完畢
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend//在緩存中尋找IMP
以上步驟: 1、判斷objc_msgSend 的第一個(gè)參數(shù),即接受者是否為空。如果為空:則跳轉(zhuǎn)到 LReturnZero流程,賦值為空。如果是TAGGED_POINTERS(小對(duì)象:如nsstring、nsnumber等類型,它們本身就是值,),則跳轉(zhuǎn)到LNilOrTagged,如果小對(duì)象為空,則也跳轉(zhuǎn)到LReturnZero,賦值為空;如果不為空,則跳轉(zhuǎn)步驟2。如果既不為空,也不是小對(duì)象,則取出isa,存入p13寄存器,執(zhí)行GetClassFromIsa_p16:也是步驟2
2、
#if SUPPORT_INDEXED_ISA
.align 3
.globl _objc_indexed_classes
_objc_indexed_classes:
.fill ISA_INDEX_COUNT, PTRSIZE, 0
#endif
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
//將isa的值存入p16寄存器
mov p16, $0 // optimistically set dst = src
//判斷是否是 nonapointer isa
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
//將_objc_indexed_classes所在的頁的基址 讀入x10寄存器 :
adrp x10, _objc_indexed_classes@PAGE
//x10基址 根據(jù) 偏移量 進(jìn)行 內(nèi)存偏移:x10 = x10 + _objc_indexed_classes(page中的偏移量)
add x10, x10, _objc_indexed_classes@PAGEOFF
//從p16的第ISA_INDEX_SHIFT位開始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0補(bǔ)充
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
//將x10+p16PTRSHIFT字節(jié)存到p16中
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__//用于64位系統(tǒng)
// 64-bit packed isa
//p16 = isa & ISA_MASK(位運(yùn)算 & 即獲取isa中的shiftcls信息)= class
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
//用于32位系統(tǒng)
mov p16, $0
#endif
.endmacro
3、第二步結(jié)束后,就執(zhí)行到CacheLookup NORMAL, _objc_msgSend(快速查找流程),搜索CacheLookup,objc-msg-arm64.s匯編文件中搜索CacheLookup,找到.macro CacheLookup定義處
.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:
//---- p1 = SEL, p16 = isa --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 從x16(即isa)中平移16字節(jié),取出cache 存入p11寄存器 -- isa距離cache 正好16字節(jié):isa(8字節(jié))-superClass(8字節(jié))-cache(mask高16位 + buckets低48位)
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
////---- 64位真機(jī)
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets 存入p10寄存器-- 即去掉mask,留下buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//--- p11(cache)右移48位,得到mask(即p11 存儲(chǔ)mask),mask & p1(msgSend的第二個(gè)參數(shù) cmd-sel) ,得到sel-imp的下標(biāo)index(即搜索下標(biāo)) 存入p12(cache insert寫入時(shí)的哈希下標(biāo)計(jì)算是 通過 sel & mask,讀取時(shí)也需要通過這種方式)
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
//--- 非64位真機(jī)
#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
//--- p12是下標(biāo) p10是buckets數(shù)組首地址,下標(biāo) * 1<<4(即16) 得到實(shí)際內(nèi)存的偏移量,通過buckets的首地址偏移,獲取bucket存入p12寄存器
//--- LSL #(1+PTRSHIFT)-- 實(shí)際含義就是得到一個(gè)bucket占用的內(nèi)存大小 -- 相當(dāng)于mask = occupied -1-- _cmd & mask -- 取余數(shù)
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//--- 從x12(即p12)中取出 bucket 分別將imp和sel 存入 p17(存儲(chǔ)imp) 和 p9(存儲(chǔ)sel)
ldp p17, p9, [x12] // {imp, sel} = *bucket
////--- 比較 sel 與 p1(傳入的參數(shù)cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
////--- 如果不相等,即沒有找到,請(qǐng)?zhí)D(zhuǎn)至 2f
b.ne 2f // scan more
//--- 如果相等 即cacheHit 緩存命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--- 如果一直都找不到,則CheckMiss, 因?yàn)槭莕ormal ,跳轉(zhuǎn)至__objc_msgSend_uncached
CheckMiss $0 // miss if bucket->sel == 0
//--- 判斷p12(下標(biāo)對(duì)應(yīng)的bucket) 是否 等于 p10(buckets數(shù)組第一個(gè)元素,),如果等于,則跳轉(zhuǎn)至第3步
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
//-- 從x12(即p12 buckets首地址)- 實(shí)際需要平移的內(nèi)存大小BUCKET_SIZE,得到得到第二個(gè)bucket元素,imp-sel分別存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
////--- 跳轉(zhuǎn)至第1步,繼續(xù)對(duì)比 sel 與 cmd
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 設(shè)置到最后一個(gè)元素
//--- p11(mask)右移44位 相當(dāng)于mask左移4位,直接定位到buckets的最后一個(gè)元素,緩存查找順序是向前查找
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.
//--- 再查找一遍緩存()
//--- 拿到x12(即p12)bucket中的 imp-sel 分別存入 p17-p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
//--- 比較 sel 與 p1(傳入的參數(shù)cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--- 如果不相等,即走到第二步
b.ne 2f // scan more
//--- 如果相等 即命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--- 如果還是沒找不到,則CheckMiss
CheckMiss $0 // miss if bucket->sel == 0
//--- 判斷p12(下標(biāo)對(duì)應(yīng)的bucket) 是否 等于 p10(buckets數(shù)組第一個(gè)元素)-- 表示前面已經(jīng)沒有了,但是還是沒有找到
cmp p12, p10 // wrap if bucket == buckets
////如果等于,跳轉(zhuǎn)至第3步
b.eq 3f
//--- 從x12(即p12 buckets首地址)- 實(shí)際需要平移的內(nèi)存大小BUCKET_SIZE,得到得到第二個(gè)bucket元素,imp-sel分別存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 跳轉(zhuǎn)至第1步,繼續(xù)對(duì)比 sel 與 cmd
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
//--- 跳轉(zhuǎn)至JumpMiss 因?yàn)槭莕ormal ,跳轉(zhuǎn)至__objc_msgSend_uncached
JumpMiss $0
.endmacro
以上其中CacheLookup NORMAL流程主要分為以下幾個(gè)步驟
- 通過類首地址平移16字節(jié)(因?yàn)樵趏bjc_class中,首地址距離cache正好16字節(jié),isa占8字節(jié),superClass占8字節(jié)),得到cahce,cache中高16位存mask,低48位存buckets,即p11 = cache
- 從
cache中分別取出buckets和mask,并由mask根據(jù)哈希算法計(jì)算出哈希下標(biāo);通過cache和掩碼(即0x0000ffffffffffff(二進(jìn)制是前16位為0,后48位為1)的 & 運(yùn)算,將高16位mask抹零,得到buckets指針地址,即p10 = buckets - 將cache
右移48位,得到mask,即p11 = mask - 將
objc_msgSend的參數(shù)p1(即第二個(gè)參數(shù)_cmd)&mask,通過哈希算法,得到需要查找存儲(chǔ)sel-imp的bucket下標(biāo)index,即p12 = index= _cmd & mask,用這個(gè)算法,是因?yàn)樵诖鎯?chǔ)sel-imp時(shí),也是通過同樣哈希算法計(jì)算哈希下標(biāo)進(jìn)行存儲(chǔ)的。 - 根據(jù)下標(biāo),在buckets里根據(jù)
index*16內(nèi)存偏移(一個(gè)bucket的大小)得到對(duì)應(yīng)存儲(chǔ)的bucket,并拿出其中的sel(p9)和_cmd進(jìn)行比較, 如果相等,說明命中了,則跳轉(zhuǎn)到CacheHit,尋找imp

如果不相等,有兩種情況
一、如果一直都找不到,直接跳轉(zhuǎn)至CheckMiss,因?yàn)槭莕ormal,會(huì)跳轉(zhuǎn)至__objc_msgSend_uncached,進(jìn)入慢速查找流程
二、【1】如果根據(jù)當(dāng)前獲取到的bucket 等于 buckets的第一個(gè)元素,則人為的將當(dāng)前bucket設(shè)置為buckets的最后一個(gè)元素(通過buckets首地址+mask右移44位(左移4位)直接定位到buckets的最后一個(gè)元素),然后繼續(xù)進(jìn)行遞歸循環(huán),即【3】
【2】如果當(dāng)前bucket不等于buckets的第一個(gè)元素,則p12向前移一個(gè)bucket位置,進(jìn)入第一次遞歸循環(huán)。
【3】第二次遞歸循環(huán):重復(fù)【2】的操作,如果當(dāng)前的bucket還是等于 buckets的第一個(gè)元素,則直接跳轉(zhuǎn)至JumpMiss,到__objc_msgSend_uncached,也進(jìn)入慢速查找流程
CheckMiss和JumpMiss具體實(shí)現(xiàn)如下,

都進(jìn)入到__objc_msgSend_uncached,即慢速查找流程
具體實(shí)現(xiàn)

以上即是整個(gè)方法快速查找流程
整個(gè)流程圖流程圖如下



