消息發(fā)送之快速轉(zhuǎn)發(fā)
在之前文章objc_class中的cache_t分析中,我們分析了,cache_t存儲
方法的過程,有留下一個疑問:cache_t
在存儲方法之前有讀取方法的,那它是怎么讀取方法的呢?
首先,讓我們先了解,什么是方法,方法的本質(zhì)
是什么。顧名思義,類或者對象調(diào)用一個API,這個API就是方法,平時我們接觸太多了,有自己定義的方法,也有調(diào)用系統(tǒng)的方法,那么這個方法在底層到底是什么?在何時調(diào)用的。
一、方法的本質(zhì)
1.1 方法是何時調(diào)用
創(chuàng)建一個
student
類,Teacher類
,student繼承Teacher。在student里聲明sayStu
方法,但沒有實現(xiàn)。> 編譯時和運行時圖-
依次
編譯
(command+B)、運行
(command+R),結(jié)果:編譯是成功的,運行時是崩潰報錯的>
方法沒實現(xiàn)@2x.png -
可以得知我們方法都是在運行時執(zhí)行的。
補充:
編譯時
和運行時
概念
編譯時
:編譯器把源代碼翻譯成機器可以識別的代碼過程。簡單的說,是個翻譯
的工作,檢查代碼里有沒有錯寫的關(guān)鍵字、詞法分析、語法分析之類靜態(tài)類型
檢查
運行時
:簡單得說,就是代碼跑起來
,被裝載到內(nèi)存里面去(代碼保存在磁盤上沒有裝入到內(nèi)存之前是個死的東西,只有到內(nèi)存
里才是活
的),如果此時出錯,則程序會崩潰,是一個動態(tài)
階段。
OC的運行時
機制:一是將數(shù)據(jù)類型的確定由編譯時,推遲到運行時。OC的這種運行時機制使對象的類型及對象的屬性和方法在運行時才能確定;二是讓OC具備多態(tài)
(不同對象以自己的方式響應(yīng)相同的消息)特性
Runtime的三種調(diào)用
:oc代碼
調(diào)用(如自定義方法)、framework
調(diào)用(系統(tǒng)方法) 、RuntimeAPI
(如class_getInstanceSize)調(diào)用
1.2 方法在底層是如何實現(xiàn)的
-
下面我們給
student
加上sayStu
的實現(xiàn),再clang
一下main.m
文件,看下底層方法是如何調(diào)用的clang
(是一個由Apple主導(dǎo)編寫,基于LLVM的C/C++/OC的編譯器):clang -rewrite-objc main.m -o main.cpp
搜索sayStu
,可以觀察到,其實是調(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>
;
手動關(guān)閉運行時的編譯警告: buildSettings->Enable Strict Checking of objc_msgSend Calls->設(shè)置為No(對objc_msgSend調(diào)用的嚴(yán)格檢查關(guān)閉)
加入測試代碼objc_msgSend(p, sel_registerName("sayStu"))
;
打印結(jié)果
三種方式調(diào)用@2x.png我們發(fā)現(xiàn),是調(diào)用成功的,三種形式的調(diào)用方式都是一個效果。
由此我們可以得出結(jié)論:方法的本質(zhì)就是方法名和對應(yīng)的函數(shù)代碼
oc層面:是對象/類+方法名
底層層面:是objc_msgSend發(fā)送消息sel->通過sel(方法編號)找到imp(函數(shù)指針地址) -> 找到函數(shù)內(nèi)容
上述流程我們唯一不清楚的是怎么通過sel找到imp的
下面就分析方法編號是如何綁定函數(shù)指針地址的
二、objc_msgSend解析
- 打開objc源碼,搜索
objc_msgsend
,發(fā)現(xiàn)objc_msgsend
是匯編語言寫的。原因是:一是整個程序方法很多,調(diào)用極其頻繁
的,所以要求速度快
。二是參數(shù)的不確定性,如果用C/C++,它們是靜態(tài)性的,速度會很慢。總體來說還是為了性能更好
。 - 還有一個疑問是,
objc_msgSend
為什么需要需要傳入?yún)?shù)消息接受者
,這是因為消息接受者里有isa
,isa
->類/元類
->cache_t(方法緩存)
->methodlist
(存在bits里) -
imp
的查找分為2個階段,快速查找
(從緩存cache中查找,匯編語言編寫)和慢速查找
(方法列表methodTable中,c/c++編寫),今天先分析快速查找
- 打開objc源碼,搜索
objc_msgsend
,選擇arm64真機環(huán)境進(jìn)行探索(其他環(huán)境也是類似),找到objc_msgSend入口
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0 和空對比,即判斷接收者是否存在,其中p0是objc_msgSend的第一個參數(shù)-消息接收者receiver
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS//支持taggedpointer(小對象類型)的流程
b.le LNilOrTagged//判斷是否是小對象或者為nil,則跳轉(zhuǎn)到LNilOrTagged// (MSB tagged pointer looks negative)//
#else
b.eq LReturnZero//p0 等于 0 時,直接返回 空
#endif
//p0即receiver 肯定存在的消息的流程
//根據(jù)對象內(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
的第一個參數(shù),即接受者是否為空
。如果為空:則跳轉(zhuǎn)到 LReturnZero
流程,賦值為空。如果是TAGGED_POINTERS(小對象:如nsstring、nsnumber等類型,它們本身就是值,),則跳轉(zhuǎn)到LNilOrTagged
,如果小對象為空,則也跳轉(zhuǎn)到LReturnZero
,賦值為空;如果不為空,則跳轉(zhuǎn)步驟2。如果既不為空,也不是小對象,則取出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補充
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(位運算 & 即獲取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位真機
#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 存儲mask),mask & p1(msgSend的第二個參數(shù) cmd-sel) ,得到sel-imp的下標(biāo)index(即搜索下標(biāo)) 存入p12(cache insert寫入時的哈希下標(biāo)計算是 通過 sel & mask,讀取時也需要通過這種方式)
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
//--- 非64位真機
#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) 得到實際內(nèi)存的偏移量,通過buckets的首地址偏移,獲取bucket存入p12寄存器
//--- LSL #(1+PTRSHIFT)-- 實際含義就是得到一個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(存儲imp) 和 p9(存儲sel)
ldp p17, p9, [x12] // {imp, sel} = *bucket
////--- 比較 sel 與 p1(傳入的參數(shù)cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
////--- 如果不相等,即沒有找到,請?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, 因為是normal ,跳轉(zhuǎn)至__objc_msgSend_uncached
CheckMiss $0 // miss if bucket->sel == 0
//--- 判斷p12(下標(biāo)對應(yīng)的bucket) 是否 等于 p10(buckets數(shù)組第一個元素,),如果等于,則跳轉(zhuǎn)至第3步
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
//-- 從x12(即p12 buckets首地址)- 實際需要平移的內(nèi)存大小BUCKET_SIZE,得到得到第二個bucket元素,imp-sel分別存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
////--- 跳轉(zhuǎn)至第1步,繼續(xù)對比 sel 與 cmd
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 設(shè)置到最后一個元素
//--- p11(mask)右移44位 相當(dāng)于mask左移4位,直接定位到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.
//--- 再查找一遍緩存()
//--- 拿到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)對應(yīng)的bucket) 是否 等于 p10(buckets數(shù)組第一個元素)-- 表示前面已經(jīng)沒有了,但是還是沒有找到
cmp p12, p10 // wrap if bucket == buckets
////如果等于,跳轉(zhuǎn)至第3步
b.eq 3f
//--- 從x12(即p12 buckets首地址)- 實際需要平移的內(nèi)存大小BUCKET_SIZE,得到得到第二個bucket元素,imp-sel分別存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 跳轉(zhuǎn)至第1步,繼續(xù)對比 sel 與 cmd
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
//--- 跳轉(zhuǎn)至JumpMiss 因為是normal ,跳轉(zhuǎn)至__objc_msgSend_uncached
JumpMiss $0
.endmacro
以上其中CacheLookup NORMAL
流程主要分為以下幾個步驟
- 通過類首地址平移16字節(jié)(因為在objc_class中,首地址距離cache正好16字節(jié),isa占8字節(jié),superClass占8字節(jié)),得到cahce,cache中高16位存mask,低48位存buckets,即p11 = cache
- 從
cache
中分別取出buckets和mask
,并由mask根據(jù)哈希算法
計算出哈希下標(biāo);通過cache
和掩碼(即0x0000ffffffffffff
(二進(jìn)制是前16位為0,后48位為1)的 & 運算,將高16位
mask抹零
,得到buckets指針地址,即p10 = buckets
- 將cache
右移48位
,得到mask,即p11 = mask
- 將
objc_msgSend
的參數(shù)p1(即第二個參數(shù)_cmd
)&mask
,通過哈希算法,得到需要查找存儲sel-imp
的bucket
下標(biāo)index
,即p12 = index
= _cmd & mask,用這個算法,是因為在存儲sel-imp時,也是通過同樣哈希算法
計算哈希下標(biāo)
進(jìn)行存儲的。 - 根據(jù)下標(biāo),在buckets里根據(jù)
index*16
內(nèi)存偏移(一個bucket的大小)得到對應(yīng)存儲的bucket,并拿出其中的sel(p9)和_cmd進(jìn)行比較, 如果相等
,說明命中了,則跳轉(zhuǎn)到CacheHit
,尋找imp
如果不相等,有兩種情況
一、如果一直都找不到,直接跳轉(zhuǎn)至CheckMiss
,因為是normal,會跳轉(zhuǎn)至__objc_msgSend_uncached,進(jìn)入慢速查找流程
二、【1】如果根據(jù)當(dāng)前獲取到的bucket 等于 buckets的第一個元素,則人為的將當(dāng)前bucket
設(shè)置為buckets的最后一個元素
(通過buckets首地址+mask右移44位
(左移4位)直接定位到buckets的最后一個元素),然后繼續(xù)進(jìn)行遞歸循環(huán),即【3】
【2】如果當(dāng)前bucket不等于buckets的第一個元素,則p12向前移一個bucket位置,進(jìn)入第一次遞歸循環(huán)。
【3】第二次遞歸循環(huán):重復(fù)【2】的操作,如果當(dāng)前的bucket
還是等于 buckets
的第一個元素
,則直接跳轉(zhuǎn)至JumpMiss
,到__objc_msgSend_uncached
,也進(jìn)入慢速查找
流程
CheckMiss
和JumpMiss
具體實現(xiàn)如下,
都進(jìn)入到__objc_msgSend_uncached
,即慢速查找流程
具體實現(xiàn)
以上即是整個方法快速查找
流程
整個流程圖流程圖如下