最近參照 MikeAsh 的這篇文章,看了 arm64
下 obj_msgSend
的實(shí)現(xiàn)蝙叛。了解了其主體流程摇予,同時(shí)對(duì)于 arm64
的匯編知識(shí)也有了更進(jìn)一步的了解。
目前最新 obj4-781 中 objc-msg-arm64.s
的實(shí)現(xiàn)彪杉,跟 MikeAsh
文中的代碼還是有些不一樣,但總體思路一致。本著學(xué)習(xí)的原則求泰,讀了最新的源碼實(shí)現(xiàn)芝加,也算檢驗(yàn)下學(xué)習(xí)成果硅卢。
總的來說,整個(gè)過程主要分為如下幾部分:
查找對(duì)象的 isa藏杖,也就是 class将塑。
查找緩存。
緩存未命中時(shí)蝌麸,走慢查找点寥。
下面來總結(jié)回顧一下。
arm64 匯編基礎(chǔ)
在閱讀之前来吩,可以先了解下 arm64 匯編的基礎(chǔ)知識(shí)敢辩。
x0 ~ x31
是通用寄存器蔽莱。而等會(huì)在下面的源碼中,我們會(huì)看到用到了 p0 而不是 x0戚长,p 代表 pointer-sized
盗冷,表示指針的大小。在 arm64 下同廉,p0 和 x0 是等價(jià)的仪糖。在 arm64-asm.h 中可以看到如下定義:
#if __LP64__
// true arm64
#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
#define p0 x0
#define p1 x1
#define p2 x2
#define p3 x3
#define p4 x4
#define p5 x5
#define p6 x6
#define p7 x7
#define p8 x8
#define p9 x9
#define p10 x10
#define p11 x11
#define p12 x12
#define p13 x13
#define p14 x14
#define p15 x15
#define p16 x16
#define p17 x17
objc_msgSend 主流程
流程介紹
整體流程如下:
我將源碼拆分成了如下 4 個(gè)文件,這樣單個(gè)處理看起來會(huì)比較清晰迫肖,文件放到了 github 上锅劝。
Entry_objc_msgSend.s,
objc_msgSend
主干流程蟆湖。GetClassFromIsa_p16.s故爵,獲取
class
流程。CacheLookup.s帐姻,緩存查找流程稠集。
LNilOrTagged.s,
TaggedPointer/nil
的處理饥瓷。
代碼實(shí)現(xiàn)
objc_msgSend
的方法定義如下:
id objc_msgSend(id self, SEL _cmd, ...);
第一個(gè)參數(shù)是 self
剥纷,第二個(gè)參數(shù)是 selector
,后面跟不定長(zhǎng)參數(shù)呢铆。當(dāng)方法被調(diào)用時(shí)晦鞋,第一個(gè)參數(shù)放入 x0
,第二個(gè)參數(shù)放入 x1
棺克。也就是 x0 = self悠垛,x1 = _cmd
。
_objc_msgSend
的匯編實(shí)現(xiàn)如下:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// 將 self 和 0 進(jìn)行比較
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
// <= 0娜谊,跳轉(zhuǎn)到 LNilOrTagged确买,進(jìn)行 nil 或者 tagged pointer 的處理。因?yàn)?tagged pointer 在 arm64 下纱皆,最高位為 1湾趾,作為有符號(hào)數(shù) < 0
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 將 isa 的值放到 x13
ldr p13, [x0] // p13 = isa
// 獲取 class 的地址,放到 p16
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 在緩存中查找或進(jìn)行完整方法查找
CacheLookup NORMAL, _objc_msgSend
下面來逐句解析下實(shí)現(xiàn)過程:
// 將 self 和 0 進(jìn)行比較
cmp p0, #0
第一步派草,將 self
和 0
比較搀缠,以便下一步進(jìn)行判定走哪種分支處理。
#if SUPPORT_TAGGED_POINTERS
// <= 0近迁,跳轉(zhuǎn)到 LNilOrTagged艺普,進(jìn)行 nil 或者 tagged pointer 的處理。因?yàn)?tagged pointer 在 arm64 下,最高位為 1歧譬,作為有符號(hào)數(shù) < 0
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
如果支持
SUPPORT_TAGGED_POINTERS
岸浑。判斷上面的比較結(jié)果,是否 ≤ 0瑰步,是則跳轉(zhuǎn)到LNilOrTagged
進(jìn)行處理助琐。因?yàn)樵?arm64 下,當(dāng)為Tagged pointer
時(shí)面氓,最高位是 1,作為有符號(hào)數(shù)蛆橡,< 0舌界。不支持的話,則判斷比較結(jié)果是否為 0泰演。如果為 0呻拌,則跳轉(zhuǎn)到
LReturnZero
進(jìn)行 nil 的處理。
關(guān)于 LNilOrTagged
的處理睦焕,我們留到后面再講藐握。先關(guān)注正常情況。
// 將 isa 的值放到 x13
ldr p13, [x0] // p13 = isa
ldr
是 Load Register
的縮寫垃喊,[]
為間接尋址猾普。它表示從 x0 所表示的地址中取出 8 字節(jié)數(shù)據(jù),放到 x13 中本谜。x0 中是 self 的地址初家,所以這里取出來的數(shù)據(jù)其實(shí)是 isa
的值。那有沒有小伙伴疑惑這是為什么呢乌助?簡(jiǎn)單解釋一下。
因?yàn)? self
是個(gè)指針,指向 struct objc_object
考蕾,它的定義如下甩苛。
struct objc_object {
private:
isa_t isa;
...
}
objc_object
中只有一個(gè)成員 isa
, 因此取出指針指向的內(nèi)容赏参,也就獲取到了 isa
的值志笼。
// 獲取 class 的地址,放到 p16
GetClassFromIsa_p16 p13 // p16 = class
調(diào)用 GetClassFromIsa_p16
登刺,進(jìn)一步獲取 class
地址籽腕。這是比較關(guān)鍵的一步,因?yàn)楹罄m(xù)操作都需要用到 class
纸俭。
獲取 class
GetClassFromIsa_p16
實(shí)現(xiàn)如下:
.macro GetClassFromIsa_p16 /* src */
// 64-bit packed isa
// p13 & 0x0000000ffffffff8ULL皇耗,獲取真正的類地址
and p16, $0, #ISA_MASK
.endmacro
源碼中關(guān)于 SUPPORT_INDEXED_ISA
的部分(這里為了簡(jiǎn)化代碼,我刪除了)揍很,標(biāo)識(shí)是否做了 isa 指針優(yōu)化郎楼。主要在 watchOS
上支持万伤,這里我們不做深究。如想了解呜袁,可以參看里面的注釋敌买。
主要思想是從 isa
中獲取 indexCls
,然后從 indexed_classes
表中獲取到 class
阶界。有興趣可以查看 runtime 源碼中 isa.h 關(guān)于 indexcls
的布局虹钮。
在 arm64 下,我們主要看這一行就好膘融。
and p16, $0, #ISA_MASK
and
表示與運(yùn)算芙粱,p16 = $0 & ISA_MASK
。$0
是傳進(jìn)來的第一個(gè)參數(shù)氧映,也就是 isa
的值春畔。那么在 64 位下,為什么要和 ISA_MASK
進(jìn)行與運(yùn)算才能獲取到呢岛都?而 ISA_MASK
的值是 0xffffffff8
律姨。
貌似我們之前的印象里,上述獲取到的 isa 就是 class 地址臼疫。這是因?yàn)槔系?struct objc_object 的類型定義如下:
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
而在 64 位中择份,isa 的類型由 Class 變?yōu)榱?union,并不直接是一個(gè)指針多矮,cls 的信息存儲(chǔ)在其中缓淹,如下所示。
struct objc_object {
private:
isa_t isa;
...
}
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
下面我們?cè)賮砜匆幌? ISA_BITFIELD
的定義塔逃,在 isa.h
中讯壶,這里是獲取 class
的關(guān)鍵。它主要定義了一些位域來存儲(chǔ)不同的信息湾盗。
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
重點(diǎn)關(guān)注第 4 項(xiàng) shiftcls
伏蚊,它占 33
位,存放的是 cls
的地址格粪。ISA_MASK
躏吊,這個(gè)掩碼就是用來取出中間的 33 位。
不知道大家有沒有跟我一樣的疑問帐萎?雖然照這樣是取出了 33 位比伏,可末尾還有 3 位是 0。那么按照常規(guī)思路疆导,不是應(yīng)該右移 3 位將其去除嗎赁项?
但事實(shí)上這樣計(jì)算是沒錯(cuò)的。因?yàn)? shiftcls
在賦值時(shí),就將地址右移了 3 位悠菜。由于是 8 字節(jié)對(duì)齊舰攒,最后 3 位肯定為 0,右移之后也無影響悔醋,到時(shí)再補(bǔ)上 0 即可摩窃。不過也看到其他文章說這樣做是為了節(jié)省空間
。所以按照上述方式取出芬骄,正好是原始地址猾愿。
到底對(duì)不對(duì),我們看下源碼就知道了账阻》梭埃可以看到在代碼最后一行,的確是右移了 3 位宰僧。
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) {
// 省略部分代碼
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
// 這里已經(jīng)右移了 3 位
newisa.shiftcls = (uintptr_t)cls >> 3;
}
這樣我們就完成了 class
地址的獲取。關(guān)于實(shí)例驗(yàn)證部分观挎,可參照我寫的這篇文章琴儿。接下來下一步,跳轉(zhuǎn)到 LGetIsaDone
嘁捷,進(jìn)行緩存的查找 CacheLookup
造成。
緩存查找
緩存結(jié)構(gòu)
由于對(duì)象的實(shí)例方法存儲(chǔ)在所屬的類中,那么必定方法緩存相關(guān)的也在類里面雄嚣。我們先看下類 objc_class
的定義晒屎,類同樣也是個(gè)對(duì)象,繼承于 objc_object
缓升。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
}
我們可以看到鼓鲁,第三項(xiàng) cache
就是緩存信息。緩存查找也就是查找這個(gè)變量中的信息港谊。cache_t
的定義如下:
struct cache_t {
// 包含 mask 和 buckets骇吭,mask 是高 16 位,剩余 48 位是 buckets 地址歧寺。
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
...
}
_maskAndBuckets
將 mask
和 buckets
的信息存放在了一起燥狰。
buckets
是哈希表,每一項(xiàng)包含了 sel 和相應(yīng)的 imp斜筐。mask
龙致,表示buckets
表的總大小,它在高 16 位顷链。
結(jié)構(gòu)如下圖所示:
其中 buckets 中每一項(xiàng) bucket_t 定義如下:
struct bucket_t {
private:
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
}
了解了這些后目代,你是不是也能大致猜到緩存是如何查找的呢?
查找過程
首先點(diǎn)擊這里查看完整的過程,代碼比較長(zhǎng)就不貼出來了像啼。(注:我將代碼中與 arm64 無關(guān)部分刪除了)俘闯。
這段稍微有點(diǎn)長(zhǎng),別擔(dān)心忽冻,下面我們來逐一分析真朗。
首先我們明確下在這個(gè)狀態(tài)下寄存器中的數(shù)據(jù)情況,即 x1 = SEL, x16 = isa
僧诚。
// p11 = [x16 + CACHE]遮婶,取出 x16+CACHE 地址中的數(shù)據(jù),8 字節(jié)
ldr p11, [x16, #CACHE]
上面我們提到過湖笨,ldr
是取數(shù)據(jù)指令旗扑。它表示從 x16 寄存器中偏移 CACHE
的位置取出一個(gè) 8 字節(jié)數(shù)據(jù),放入 p11
中慈省。x16 中存放的是 class 地址臀防。在 objc-msg-arm64.s 中,CACHE
的定義如下边败,2 個(gè)指針大小袱衷,也就是 16 字節(jié)。
#define CACHE (2 * __SIZEOF_POINTER__)
為什么要偏移 16 字節(jié)呢笑窜?我們?cè)俅蝸砜聪?objc_object 的定義致燥。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
}
從定義可以看出,cache 的偏移是 16 字節(jié)排截,在 ISA
和 superclass
之后嫌蚤。
因此,x16 + #CACHE
正好指向 cache_t
断傲。此時(shí)再取出 8 字節(jié)的內(nèi)容脱吱,就得到了 cache_t
結(jié)構(gòu)的第一項(xiàng) _maskAndBuckets
。忘記的同學(xué)可往上翻看 cache_t
的定義认罩。
這樣急凰,p11
中存放的是 _maskAndBuckets
的值。
// p10 = p11 & 0x0000ffffffffffff猜年,取出 buckets 地址
and p10, p11, #0x0000ffffffffffff
由于 buckets 是低 48 位抡锈,將 p11 進(jìn)行與運(yùn)算得到 buckets 地址。
// 前 16 位是 mask乔外,表示緩存表總共有多少項(xiàng)床三,x1 里面是 _cmd,根據(jù) _cmd & mask 求出 _cmd 在表中對(duì)應(yīng)的 index杨幼。
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
LSR
表示邏輯右移撇簿,即 p11 右移 48 位后聂渊,再與 p1 進(jìn)行與運(yùn)算。若進(jìn)行步驟拆分四瘫,可表示為如下:
// 得到 mask 的值
p11 = p11 >> 48
// 求出 index
p12 = p1 & p11
這時(shí)候汉嗽,p11 是 mask 的值,也就是總表項(xiàng)大小找蜜。p1 是 _cmd
饼暑,_cmd & mask
是為了求出 _cmd
在表中的索引,也就等同于 _cmd % mask
洗做。
// PTRSHIFT = 3弓叛,表中每一項(xiàng)大小為 16 字節(jié),左移 4 位诚纸,相當(dāng)于乘以 16撰筷。獲取 index 對(duì)應(yīng)項(xiàng)的地址,放到 x12畦徘。
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
add p12, p10, p12, LSL #(1+PTRSHIFT)
這步是為了求出索引在緩存表中的表項(xiàng)地址毕籽。
LSL
表示邏輯左移,PTRSHIFT = 3
井辆,也就是 p12 <<= 4
影钉,相當(dāng)于乘以 16。因?yàn)楸碇忻宽?xiàng)大小為 16 字節(jié)掘剪。而 p10
表示緩存表地址,加上偏移量奈虾,就可得到索引項(xiàng)地址夺谁,再放入 p12
中。
// 從定位到的表項(xiàng)地址中肉微,取出 2 個(gè) 8 字節(jié)數(shù)據(jù)放到 p17, p9 中匾鸥。其中 p17 里是 imp,p9 里是 sel碉纳。
ldp p17, p9, [x12] // {imp, sel} = *bucket
ldp
是 Load Register Pair
的縮寫勿负,它表示從 x12 所表示的地址中取出 2 個(gè) 8 字節(jié)數(shù)據(jù),分別放入 p17劳曹、p9 中奴愉。從 bucket_t 的定義,我們可以得知緩存表中的每一項(xiàng)是 {imp, sel}
铁孵。正好得到 p17 = imp锭硼,p9 = sel
。
// 比較緩存中的 sel 和傳入的 _cmd
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 不相等蜕劝,跳轉(zhuǎn)到 2
b.ne 2f // scan more
// 命中緩存檀头,調(diào)用 imp
CacheHit $0 // call or return imp
接著轰异,將 p9 中的 sel 與傳入的 _cmd 進(jìn)行比較,如果不相等暑始,則跳轉(zhuǎn)到 2 處理搭独,繼續(xù)掃描;如果相等廊镜,則表示命中緩存牙肝。這里我們先不看 2 的處理,緊接著看看緩存命中的實(shí)現(xiàn)期升。
緩存命中
命中緩存會(huì)調(diào)用 CacheHit
惊奇,其實(shí)現(xiàn)代碼如下(刪除了無關(guān)代碼):
// 命中緩存
.macro CacheHit
// 調(diào)用 imp
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.endmacro
其實(shí),這里的處理很簡(jiǎn)單播赁,就是進(jìn)一步調(diào)用 TailCallCachedImp
颂郎,此時(shí)各個(gè)寄存器值如下。
x17 = 緩存 imp
x12 = 查找到的緩存項(xiàng)地址
x1 = sel
x16 = class
TailCallCachedImp
的實(shí)現(xiàn)如下:
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor $0, $0, $3
br $0
.endmacro
最終使用 br
指令調(diào)用 $0
容为,br 表示有返回的跳轉(zhuǎn)乓序,可認(rèn)為是 x86
中的 call
指令。而 $0
是查找到的緩存 imp
坎背。這樣緩存命中的分支就走完了替劈,最終調(diào)用 imp。
緩存表掃描過程
當(dāng)?shù)谝徊骄彺嫖疵袝r(shí)得滤,又是如何處理呢陨献?這步會(huì)復(fù)雜一些,因?yàn)樯婕暗絻纱尉彺姹頀呙琛?/p>
從索引對(duì)應(yīng)的緩存項(xiàng)不斷向上查找懂更,直到表頭眨业。
當(dāng)?shù)竭_(dá)表頭后,繼續(xù)從表尾開始全表掃描沮协,直至重新回到表頭龄捡。
過程如下圖所示:
前面我們說到,當(dāng)未命中時(shí)慷暂,會(huì)跳轉(zhuǎn)到 2 去繼續(xù)掃描緩存表聘殖。2 的內(nè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
首先用 CheckMiss
檢查緩存表項(xiàng)是否為空。
- 如果為空行瑞,則進(jìn)行
__objc_msgSend_uncached
的查找奸腺。
CheckMiss
實(shí)現(xiàn)如下:
// 檢查緩存項(xiàng)是否為空
.macro CheckMiss
// 檢查 p9 中的 sel 是否為空,若為空血久,則跳轉(zhuǎn)到 __objc_msgSend_uncached洋机,再進(jìn)行緩存未命中的查找
cbz p9, __objc_msgSend_uncached
.endmacro
cbz
判斷 p9 是否為空,p9 表示 sel洋魂。也就是說绷旗,緩存表中這一項(xiàng)是空的喜鼓,會(huì)進(jìn)行 c 方法的慢查找。
- 如果不為空衔肢,則會(huì)進(jìn)行如下判斷庄岖。
cmp p12, p10
b.eq 3f
p10 是緩存表地址,這里判斷當(dāng)前緩存表項(xiàng)是否是表頭角骤。如果不是隅忿,則循環(huán)往前遍歷緩存表,不斷的進(jìn)行比較邦尊。
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
// 循環(huán)進(jìn)行比較
b 1b // loop
ldp 最后跟了一個(gè) 背桐!
,它表示將 x12 減去 BUCKET_SIZE
蝉揍,然后寫回到 x12 中链峭。分步表示如下:
x12 -= BUCKET_SIZE
ldp p17, p9, [x12]
若當(dāng)前緩存表項(xiàng)是表頭時(shí),會(huì)跳轉(zhuǎn)到 3 進(jìn)行如下處理:
3: // wrap: p12 = first bucket, w11 = mask
// p12 = buckets + (mask << 1+PTRSHIFT)
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
將 p12 指向表尾又沾,然后從表尾向表頭遍歷比較弊仪。
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
這句可能不太好理解,下面來解釋一下杖刷。
因?yàn)?mask 是高 16 位励饵,需右移 48 位得到 mask 大小。而每項(xiàng)大小是 16 字節(jié)滑燃,需左移 4 位得到整個(gè)表偏移役听,因此總共需右移 44 位。分步表示如下:
mask = p11 >> 48
offset = mask << 4
那么為什么在緩存表項(xiàng)是表頭時(shí)表窘,需要再次掃描緩存表呢咕娄?代碼中有如下注釋:
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
重復(fù)掃描是為了處理緩存被破壞時(shí)的情況蜒茄。
因?yàn)檎G闆r下际长,緩存表中馍迄,要么是有效數(shù)據(jù)艳吠,要么是空表項(xiàng)麦备,這兩種結(jié)果都會(huì)退出循環(huán)。而當(dāng)緩存破壞時(shí)昭娩,會(huì)存在三種結(jié)果凛篙,有效數(shù)據(jù)/無效數(shù)據(jù)/空表項(xiàng)。當(dāng)為無效數(shù)據(jù)時(shí)栏渺,肯定與 sel 不匹配呛梆。假設(shè)在極端情況下,緩存被破壞磕诊,這樣會(huì)導(dǎo)致一直查找到表頭都是無效數(shù)據(jù)項(xiàng)填物。
所以蘋果工程師們纹腌,做了這樣一種補(bǔ)救措施。當(dāng)?shù)谝淮螐亩ㄎ坏木彺姹眄?xiàng)反向掃描到表頭后滞磺,重新從表尾開始掃描升薯,進(jìn)行全表查找。當(dāng)?shù)诙卧俅螔呙璧奖眍^時(shí)击困,就會(huì)跳轉(zhuǎn)到 JumpMiss
涎劈,表示緩存未命中,進(jìn)入慢查找過程更新緩存阅茶。
最后看下這個(gè)過程的完整代碼:
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 不相等蛛枚,跳轉(zhuǎn)到 2
b.ne 2f // scan more
// 命中緩存,調(diào)用 imp
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, p11, LSR #(48 - (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
// 比較取到的緩存項(xiàng)和緩存表地址是否一致脸哀,也就是是否是第一項(xiàng)
cmp p12, p10 // wrap if bucket == buckets
// 相等蹦浦,跳轉(zhuǎn)到 3
b.eq 3f
// 從當(dāng)前表項(xiàng)開始,繼續(xù)往上找上一項(xiàng)
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
// 然后再次跳轉(zhuǎn)到 1企蹭,進(jìn)行循環(huán)比較
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
從代碼中可以看到上下分別有 1白筹、2、3 標(biāo)簽谅摄。下面的 1徒河、2 跟上面的 1、2 處理過程很類似送漠,只不過下面的 2 跳轉(zhuǎn)到 3 的處理有點(diǎn)不一樣顽照。
3: // double wrap
JumpMiss $0
下面 2 中同樣是判斷表項(xiàng)與表頭是否相等,若相等闽寡,表示已經(jīng)遍歷完全表代兵,但仍未找到緩存,則跳入到 JumpMiss
的處理爷狈。同樣植影,JumpMiss
會(huì)調(diào)用 __objc_msgSend_uncached
。
// 緩存未命中的處理
.macro JumpMiss
// 調(diào)用 __objc_msgSend_uncached涎永,進(jìn)行緩存未命中的查找
b __objc_msgSend_uncached
.endif
緩存未命中處理
緩存未命中時(shí)思币,都會(huì)走到 __objc_msgSend_uncached
去處理。
__objc_msgSend_uncached
的實(shí)現(xiàn)很簡(jiǎn)單羡微,調(diào)用 MethodTableLookup
進(jìn)行方法查找谷饿。
// 緩存未命中的查找
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
MethodTableLookup
的實(shí)現(xiàn)如下:
// 方法查找
.macro MethodTableLookup
// 保存寄存器
SAVE_REGS
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
// 第 3 個(gè)參數(shù)是 cls,x16 中保存了 cls
mov x2, x16
// LOOKUP_INITIALIZE = 1, LOOKUP_RESOLVER = 2, 兩者或運(yùn)算 = 3
mov x3, #3
// 調(diào)用 _lookUpImpOrForward 進(jìn)行查找妈倔,最后查找到的 imp 放到 x0 中
bl _lookUpImpOrForward
// IMP in x0
// 將 imp 放到 x17
mov x17, x0
// 恢復(fù)寄存器
RESTORE_REGS
.endmacro
它主要做了如下事情:
保存寄存器
設(shè)置
lookUpImpOrForward
調(diào)用所需的參數(shù)博投,函數(shù)調(diào)用時(shí)前 8 個(gè)參數(shù)放在 x0 ~ x7 中。調(diào)用
lookUpImpOrForward
進(jìn)行方法查找由于返回結(jié)果是放在 x0 中盯蝴,之前緩存查找結(jié)果的 imp 是放在 x17 中毅哗,這里保持一致
恢復(fù)寄存器
所以最終是調(diào)用到 lookUpImpOrForward
去進(jìn)行方法的查找听怕,在 objc-runtime-new.mm 中有相應(yīng)的實(shí)現(xiàn)。
通過 lookUpImpOrForward
查找到結(jié)果后黎做,會(huì)調(diào)用 TailCallFunctionPointer x17
來完成最后一步使命叉跛。
TailCallFunctionPointer
實(shí)現(xiàn)如下,可見它也只是執(zhí)行 br
指令蒸殿,調(diào)用傳入的 imp
筷厘。
.macro TailCallFunctionPointer
// $0 = function pointer value
br $0
.endmacro
到這里,完整的流程就走完了宏所。但是酥艳,還未完,在文章開頭我們跳過了 Tagged pointer
和 nil
的處理爬骤,即跳轉(zhuǎn)到 LNilOrTagged
充石。
Tagged Pointer 處理
為什么 Tagged Pointer
要單獨(dú)拎出來呢?由于 Tagged Pointer
的特殊性霞玄,它本身不是個(gè)指針骤铃,而是存儲(chǔ)了實(shí)際的數(shù)據(jù),其 class 地址的獲取方式跟普通對(duì)象不一樣坷剧,所以需要單獨(dú)處理惰爬。
而 Tagged Pointer
分為 Extend Tagged Pointer
和 Basic Tagged Pointer
,并且兩者的內(nèi)存布局不太一樣惫企,這無疑又增加了復(fù)雜度撕瞧,需分別處理。
Tagged Pointer
下面先簡(jiǎn)要介紹一下 Tagged Pointer
狞尔,它是一項(xiàng)在 64 位下節(jié)省內(nèi)存空間的技術(shù)丛版。當(dāng)使用一些小對(duì)象,比如 NSNumber
偏序、NSDate
時(shí)页畦,可能它們的值用不著 64 位來表示。如果使用普通對(duì)象的存儲(chǔ)方式研儒,需要分配的內(nèi)存空間 = 指針 8 字節(jié) + 對(duì)象大小豫缨,會(huì)很浪費(fèi)空間。這時(shí)候殉摔,我們可以做一些優(yōu)化州胳,在 64 位中分配一些位用于存儲(chǔ)數(shù)據(jù)记焊,然后做一些標(biāo)記逸月,表示它非普通對(duì)象的指針。此時(shí)遍膜,它不再是一個(gè)指針碗硬,不指向任何內(nèi)存地址瓤湘,而是真實(shí)的值。當(dāng)然恩尾,如果數(shù)值較大弛说,還是會(huì)用普通對(duì)象存儲(chǔ)。
判斷 Tagged Pointer
在代碼的最開頭部分翰意,判定 self ≤ 0
跳入 LNilOrTagged
的處理木人。我們說當(dāng)小于 0 時(shí),表示是 Tagged Pointer
冀偶。這一點(diǎn)醒第,從源碼中可以找到答案。
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
在 arm64
下进鸠,_OBJC_TAG_MASK
定義如下稠曼。
#define _OBJC_TAG_MASK (1UL<<63)
那么根據(jù)上述代碼,我們可以得知:
在 arm64 下客年,當(dāng)最高位為 1 時(shí)霞幅,則為 Tagged Pointer
。
另外量瓜,Tagged Pointer
中還會(huì)區(qū)分是否為 Extend Tagged Pointer
司恳,實(shí)現(xiàn)在 objc-object.h
中。
inline bool
objc_object::isExtTaggedPointer()
{
uintptr_t ptr = _objc_decodeTaggedPointer(this);
return (ptr & _OBJC_TAG_EXT_MASK) == _OBJC_TAG_EXT_MASK;
}
其中榔至,_OBJC_TAG_EXT_MASK
定義如下抵赢,在 objc-internal.h
。
#define _OBJC_TAG_EXT_MASK (0xfUL<<60)
若高 4 位是全為 1唧取,則表示 Extend Tagged Pointer
铅鲤。
class index
Tagged Pointer
中記錄了對(duì)象指向的 class
的索引信息,可根據(jù)索引到 Tagged Pointer Table
中查找到對(duì)應(yīng)的 class
枫弟。索引的信息的布局根據(jù)是否為 Extend Tagged Pointer
有所不同邢享。
如果是普通的
Tagged Pointer
,高 4 位為索引淡诗,到Tagged Pointer Table
中查找class
骇塘。如果是
Extend Tagged Pointer
,由于高 4 位都為 1韩容,那么接下來的 8 位表示索引款违,到Extend Tagged Pointer Table
表中查找。
對(duì)象布局如下圖所示:
獲取 index 的方式群凶,我們從通過源碼中 objc-object.h 可以得到驗(yàn)證插爹。下面代碼中 slot 就代表 index。
#define _OBJC_TAG_SLOT_SHIFT 60
#define _OBJC_TAG_SLOT_MASK 0xf
#define _OBJC_TAG_EXT_SLOT_SHIFT 52
#define _OBJC_TAG_EXT_SLOT_MASK 0xff
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
// 右移 60 位,獲取高 4 位
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
// 如果是 extend tagged pointer赠尾,則獲取到的 cls 的特殊的 ___NSUnrecognizedTaggedPointer
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
// 獲取 extend 的索引力穗,右移 52 位后,取低 8 位
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
下面將其拆分來解釋一下气嫁。
#define _OBJC_TAG_SLOT_SHIFT 60
#define _OBJC_TAG_SLOT_MASK 0xf
// 右移 60 位当窗,獲取高 4 位
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
這一步獲取 index。將指針右移 60 位寸宵,得到高 4 位地址崖面,然后跟掩碼 0xf
做與運(yùn)算。
cls = objc_tag_classes[slot];
從 objc_tag_classes
表中獲取到 cls梯影。
// 如果是 extend tagged pointer嘶朱,則獲取到的 cls 的特殊的 ___NSUnrecognizedTaggedPointer
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
...
}
接著判斷是否為 OBJC_CLASS_$___NSUnrecognizedTaggedPointer。
為什么要判斷呢 __NSUnrecognizedTaggedPointer
光酣?在 NSObject.mm 中有這樣一段注釋疏遏。大致意思是它是作為舊調(diào)試器的占位符,當(dāng)檢查 extend tagged pointer
時(shí)救军,得到的 cls
會(huì)是 __NSUnrecognizedTaggedPointer
财异。
// Placeholder for old debuggers. When they inspect an
// extended tagged pointer object they will see this isa.
@interface __NSUnrecognizedTaggedPointer : NSObject
@end
前面我們說過高 4 位是 tagged pointer
的索引。當(dāng)全為 1 時(shí)唱遭,則表示是 extend tagged pointer
戳寸。所以用了一個(gè)占位的 cls 來表示是 extend 類型。
如果是 __NSUnrecognizedTaggedPointer
拷泽,表明它是 Extend Tagged Pointer
疫鹊,需要再取出 extend index
。
#define _OBJC_TAG_EXT_SLOT_SHIFT 52
#define _OBJC_TAG_EXT_SLOT_MASK 0xff
// 獲取 extend 的索引司致,右移 52 位后拆吆,取低 8 位
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
這一步,指針右移 52
位脂矫,與上 0xff
枣耀,獲取 8
位的索引。最后從 objc_tag_ext_classes
表中獲取到 cls庭再。
這一段的源碼分析對(duì)于理解下一節(jié)中的流程處理很有幫助捞奕,因?yàn)閰R編也是按照這個(gè)方式來處理的。
處理流程
LNilOrTagged
的處理如下:
LNilOrTagged:
// 如果是 nil拄轻,跳轉(zhuǎn) LReturnZero 處理
b.eq LReturnZero // nil check
// tagged
// 獲取 _objc_debug_taggedpointer_classes 表地址颅围,放入 x10
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
// 從 x0 中,提取 60 ~ 63 位恨搓,也就是 索引值院促,放入 x11
ubfx x11, x0, #60, #4
// 從表中取出索引對(duì)應(yīng)的項(xiàng),也就是 class 地址,放入 x16一疯。由于每項(xiàng)為 8 字節(jié),所以左移 3 位
ldr x16, [x10, x11, LSL #3]
// 獲取 _OBJC_CLASS_$___NSUnrecognizedTaggedPointer 地址
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
// 將取出的 class 地址與 NSUnrecognizedTaggedPointer 地址進(jìn)行比較
cmp x10, x16
// 不相等夺姑,則跳回主流程墩邀,進(jìn)行緩存查找或者方法查找
b.ne LGetIsaDone
// ext tagged
// 如果相等,那么表示它是 extend tagged pointer盏浙,取出 _objc_debug_taggedpointer_ext_classes 地址放到 X10
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
// 從 x0 中眉睹,提取 52 ~ 59 位,得到索引值
ubfx x11, x0, #52, #8
// 獲取 class 的地址
ldr x16, [x10, x11, LSL #3]
// 跳回主流程
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
我們來一步步分析:
// 如果是 nil废膘,跳轉(zhuǎn) LReturnZero 處理
b.eq LReturnZero // nil check
判定 self 與 0 的比較結(jié)果是否相等竹海,是則跳轉(zhuǎn) nil 處理。
// 獲取 _objc_debug_taggedpointer_classes 表地址丐黄,放入 x10
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
這里通過 adrp
和 add
兩條指令斋配,獲取 _objc_debug_taggedpointer_classes
的地址。因?yàn)?arm64 指令是固定長(zhǎng)度 32 位灌闺,操作數(shù)中不能放下 64 位的地址艰争。因此先用 adrp 來獲取地址的高 32 位部分,然后再加上低 32 位桂对,放入到 x10甩卓。
// 從 x0 中,提取 60 ~ 63 位蕉斜,也就是 索引值逾柿,放入 x11
ubfx x11, x0, #60, #4
ubfx
是字節(jié)提取指令,從第 60 位開始宅此,總共提取 4 位机错。這里 x0 = self
,從 x0 中提取高 4 位父腕,放入 x11毡熏。
// 從表中取出索引對(duì)應(yīng)的項(xiàng),也就是 class 地址侣诵,放入 x16痢法。由于每項(xiàng)為 8 字節(jié),所以左移 3 位
ldr x16, [x10, x11, LSL #3]
ldr
指令我們應(yīng)該比較熟悉杜顺,上面用到很多财搁。_objc_debug_taggedpointer_classes
表項(xiàng)大小為 8, 所以 x11 左移 3 位躬络,計(jì)算相對(duì)于表頭的偏移尖奔,然后將地址中的數(shù)據(jù)放到 x16 中。這里就獲取到了 class 地址。
// 獲取 _OBJC_CLASS_$___NSUnrecognizedTaggedPointer 地址
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
// 將取出的 class 地址與 NSUnrecognizedTaggedPointer 地址進(jìn)行比較
cmp x10, x16
// 不相等提茁,則跳回主流程淹禾,進(jìn)行緩存查找或者方法查找
b.ne LGetIsaDone
這里,以同樣的方式獲取到 _OBJC_CLASS_$___NSUnrecognizedTaggedPointer
的地址茴扁。用來判定是否是 extend tagged pinter
铃岔。
如果不是,則跳回主流程峭火,執(zhí)行我們前面部分講解的過程毁习。
// 如果相等,那么表示它是 extend tagged pointer卖丸,取出 _objc_debug_taggedpointer_ext_classes 地址放到 X10
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
// 從 x0 中纺且,提取 52 ~ 59 位,得到索引值
ubfx x11, x0, #52, #8
// 獲取 class 的地址
ldr x16, [x10, x11, LSL #3]
// 跳回主流程
b LGetIsaDone
否則表明它是 extend tagged pointer
稍浆,得從 _objc_debug_taggedpointer_ext_classes
獲取 class
地址载碌。
而 extend tagged pointer
的索引位在高 4 位后面接下來的 8 位,也就是 52 ~ 59
位中衅枫,進(jìn)行上述類似的提取過程恐仑,然后獲取 class 的地址,放到 x16 中为鳄,跳回主流程繼續(xù)處理裳仆。
nil 處理
nil 的處理如下,比較簡(jiǎn)單孤钦,將可能用于存儲(chǔ)函數(shù)返回值的寄存器清 0歧斟。
LReturnZero:
// x0 is already zero
// 將寄存器清 0
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
x0 和 x1 用來存儲(chǔ)整形返回值,v0 ~ v3 用來存儲(chǔ)浮點(diǎn)型返回值偏形,d0 ~ d3 表示其低 32 位静袖。
總結(jié)
這里我將 objc_msgSend
的流程大致捋了一遍,包括 class 查找俊扭、緩存查找队橙、緩存未命中的處理、taggedPointer 和 nil 處理萨惑。對(duì)于 class 查找的過程捐康,是比較核心的一部分。不同類型的對(duì)象有著不同的查找方式庸蔼,相信如果弄懂了這部分解总,對(duì)于對(duì)象結(jié)構(gòu)的布局會(huì)有進(jìn)一步的理解。
另外姐仅,看 objc_msgSend
的源碼對(duì)于學(xué)習(xí) arm64
匯編的基礎(chǔ)指令也是一種比較好的途徑花枫,因?yàn)榇蟛糠滞瑢W(xué)對(duì)于 x86
的指令會(huì)熟悉一些刻盐。雖然匯編起初會(huì)讓人覺得云里霧里,還沒看就放棄劳翰。但是如果一句句讀下來敦锌,你會(huì)發(fā)現(xiàn)它和平常我們寫的代碼邏輯也沒啥兩樣。
最后佳簸,希望這篇文章能帶給你一些不同的知識(shí)乙墙。