arm64 objc_msgSend 源碼解讀

最近參照 MikeAsh 的這篇文章,看了 arm64obj_msgSend 的實(shí)現(xiàn)蝙叛。了解了其主體流程摇予,同時(shí)對(duì)于 arm64 的匯編知識(shí)也有了更進(jìn)一步的了解。

目前最新 obj4-781objc-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 主流程

流程介紹

整體流程如下:

image

我將源碼拆分成了如下 4 個(gè)文件,這樣單個(gè)處理看起來會(huì)比較清晰迫肖,文件放到了 github 上锅劝。

代碼實(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

第一步派草,將 self0 比較搀缠,以便下一步進(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

ldrLoad 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;

...

}

_maskAndBucketsmaskbuckets 的信息存放在了一起燥狰。

  • buckets 是哈希表,每一項(xiàng)包含了 sel 和相應(yīng)的 imp斜筐。

  • mask龙致,表示 buckets 表的總大小,它在高 16 位顷链。

結(jié)構(gòu)如下圖所示:

image

其中 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é)排截,在 ISAsuperclass 之后嫌蚤。

因此,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

ldpLoad 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>

  1. 從索引對(duì)應(yīng)的緩存項(xiàng)不斷向上查找懂更,直到表頭眨业。

  2. 當(dāng)?shù)竭_(dá)表頭后,繼續(xù)從表尾開始全表掃描沮协,直至重新回到表頭龄捡。

過程如下圖所示:

image

前面我們說到,當(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 pointernil 的處理爬骤,即跳轉(zhuǎn)到 LNilOrTagged充石。

Tagged Pointer 處理

為什么 Tagged Pointer 要單獨(dú)拎出來呢?由于 Tagged Pointer 的特殊性霞玄,它本身不是個(gè)指針骤铃,而是存儲(chǔ)了實(shí)際的數(shù)據(jù),其 class 地址的獲取方式跟普通對(duì)象不一樣坷剧,所以需要單獨(dú)處理惰爬。

Tagged Pointer 分為 Extend Tagged PointerBasic 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ì)象布局如下圖所示:

image

獲取 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

這里通過 adrpadd 兩條指令斋配,獲取 _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í)乙墙。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市溺蕉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌悼做,老刑警劉巖疯特,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異肛走,居然都是意外死亡漓雅,警方通過查閱死者的電腦和手機(jī)缸棵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門放坏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來栖榨,“玉大人维蒙,你說我怎么就攤上這事则剃〕嵊” “怎么了汗菜?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵添诉,是天一觀的道長(zhǎng)梢褐。 經(jīng)常有香客問我旺遮,道長(zhǎng),這世上最難降的妖魔是什么盈咳? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任耿眉,我火速辦了婚禮,結(jié)果婚禮上鱼响,老公的妹妹穿的比我還像新娘鸣剪。我一直安慰自己,他們只是感情好丈积,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布筐骇。 她就那樣靜靜地躺著,像睡著了一般江滨。 火紅的嫁衣襯著肌膚如雪拥褂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天牙寞,我揣著相機(jī)與錄音饺鹃,去河邊找鬼莫秆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛悔详,可吹牛的內(nèi)容都是我干的镊屎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼茄螃,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼缝驳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起归苍,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤用狱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后拼弃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體夏伊,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年吻氧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了溺忧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盯孙,死狀恐怖鲁森,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情振惰,我是刑警寧澤歌溉,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站骑晶,受9級(jí)特大地震影響研底,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜透罢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一榜晦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧羽圃,春花似錦乾胶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至脑融,卻和暖如春喻频,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肘迎。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工甥温, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锻煌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓姻蚓,卻偏偏與公主長(zhǎng)得像宋梧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子狰挡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 概覽 每個(gè)Objective-C對(duì)象都有相應(yīng)的類捂龄,這個(gè)類都有一個(gè)方法列表。類中的每個(gè)方法都有一個(gè)選擇子加叁、一個(gè)指向方...
    alvin_wang閱讀 758評(píng)論 0 0
  • objc_msgSend是OC中調(diào)用最為頻繁的方法倦沧,所有OC方法的調(diào)用都離不開這個(gè)它。蘋果已經(jīng)將其開源(https...
    某某香腸閱讀 766評(píng)論 0 0
  • 閱讀本文后你將會(huì)進(jìn)一步了解Runtime的實(shí)現(xiàn)它匕,享元設(shè)計(jì)模式的實(shí)踐展融,內(nèi)存數(shù)據(jù)存儲(chǔ)優(yōu)化,編譯內(nèi)存屏障超凳,多線程無鎖讀寫...
    歐陽大哥2013閱讀 17,662評(píng)論 19 124
  • 引入 眾所周知愈污,Objective-C動(dòng)態(tài)性的根源在方法的調(diào)用是通過message來實(shí)現(xiàn)的耀态,一次發(fā)生message...
    吸血鬼de晚餐閱讀 5,424評(píng)論 8 9
  • 一轮傍、clang指令探查方法調(diào)用 Clang是一個(gè)由Apple主導(dǎo)編寫,基于LLVM的C/C++/Objective...
    風(fēng)緊扯呼閱讀 895評(píng)論 0 3