objc_msgSend

動(dòng)手實(shí)現(xiàn) objc_msgSend

objc_msgSend 函數(shù)支撐了我們使用 Objective-C 實(shí)現(xiàn)的一切。要理解某件事還有比自己動(dòng)手實(shí)現(xiàn)一次更好的方法嗎?咱們來自己動(dòng)手實(shí)現(xiàn)一個(gè) objc_msgSend。

Tramapoline! Trampopoline! (蹦床)

當(dāng)你寫了一個(gè)發(fā)送 Objective-C 消息的方法:

[obj message]

編譯器會(huì)生成一個(gè) objc_msgSend 調(diào)用:

objc_msgSend(obj, @selector(message));

之后 objc_msgSend 會(huì)負(fù)責(zé)轉(zhuǎn)發(fā)這個(gè)消息。

它都做了什么?它會(huì)查找合適的函數(shù)指針或者 IMP,然后調(diào)用,最后跳轉(zhuǎn)。任何傳給 objc_msgSend 的參數(shù),最終都會(huì)成為 IMP 的參數(shù)。 IMP 的返回值成為了最開始被調(diào)用的方法的返回值。

因?yàn)?objc_msgSend 只是負(fù)責(zé)接收參數(shù),找到合適的函數(shù)指針,然后跳轉(zhuǎn),有時(shí)管這種叫做 trampoline(譯注:[蹦床](https://en.wikipedia.org/wiki/Trampoline_(computing)). 更通用的來說,任何一段負(fù)責(zé)把一段代碼轉(zhuǎn)發(fā)到另一處的代碼,都可以被叫做 trampoline。

這種轉(zhuǎn)發(fā)的行為使 objc_msgSend 變得特殊起來。因?yàn)樗皇呛?jiǎn)單的查找合適的代碼,然后直接跳轉(zhuǎn)過去,這相當(dāng)?shù)耐ㄓ?。傳入任何參?shù)組合都可以,因?yàn)樗皇前堰@些參數(shù)留給 IMP 去讀取。返回值有些棘手,但最終都可以看成 objc_msgSend 的不同變種。

不幸的是,這些轉(zhuǎn)發(fā)行為都不能用純 C 實(shí)現(xiàn)。因?yàn)闆]有方法可以將傳入 C 函數(shù)的泛參(generic parameters)傳給另一個(gè)函數(shù)。 你可以使用變參,但是變參和普通參數(shù)的傳遞方法不同,而且慢,所以這不適合普通的 C 參數(shù)。

如果要用 C 來實(shí)現(xiàn) objc_msgSend,基本樣子應(yīng)該像這樣:

id objc_msgSend(id self, SEL _cmd, ...)

{

Class c = object_getClass(self);

IMP imp = class_getMethodImplementation(c, _cmd);

return imp(self, _cmd, ...);

}

這有點(diǎn)過于簡(jiǎn)單。事實(shí)上會(huì)有一個(gè)方法緩存來提升查找速度,像這樣:

id objc_msgSend(id self, SEL _cmd, ...)

{

Class c = object_getClass(self);

IMP imp = cache_lookup(c, _cmd);

if(!imp)

imp = class_getMethodImplementation(c, _cmd);

return imp(self, _cmd, ...);

}

通常為了速度,cache_lookup 使用 inline 函數(shù)實(shí)現(xiàn)。

匯編

在 Apple 版的 runtime 中,為了最大化速度,整個(gè)函數(shù)是使用匯編實(shí)現(xiàn)的。在 Objective-C 中每次發(fā)送消息都會(huì)調(diào)用 objc_msgSend,在一個(gè)應(yīng)用中最簡(jiǎn)單的動(dòng)作都會(huì)有成千或者上百萬的消息。

為了讓事情更簡(jiǎn)單,我自己的實(shí)現(xiàn)中會(huì)盡可能少的使用匯編,使用獨(dú)立的 C 函數(shù)抽象復(fù)雜度。匯編代碼會(huì)實(shí)現(xiàn)下面的功能:

id objc_msgSend(id self, SEL _cmd, ...)

{

IMP imp = GetImplementation(self, _cmd);

imp(self, _cmd, ...);

}

GetImplementation 可以用更可讀的方式工作。

匯編代碼需要:

1. 把所有潛在的參數(shù)存儲(chǔ)在安全的地方,確保 GetImplementation 不會(huì)覆蓋它們。

2. 調(diào)用 GetImplementation。

3. 把返回值保存在某處。

4. 恢復(fù)所有的參數(shù)值。

5. 跳轉(zhuǎn)到 GetImplementation 返回的 IMP。

讓我們開始吧!

這里我會(huì)嘗試使用 x86-64 匯編,這樣可以很方便的在 Mac 上工作。這些概念也可以應(yīng)用于 i386 或者 ARM。

這個(gè)函數(shù)會(huì)保存在獨(dú)立的文件中,叫做 msgsend-asm.s。這個(gè)文件可以像源文件那樣傳遞給編譯器,然后會(huì)被編譯并鏈接到程序中。

第一件事要做的是聲明全局的符號(hào)(global symbol)。因?yàn)橐恍o聊的歷史原因,C 函數(shù)的 global symbol 會(huì)在名字前有個(gè)下劃線:

.globl _objc_msgSend

_objc_msgSend:

編譯器會(huì)很高興的鏈接最近可使用的(nearest available) objc_msgSend。簡(jiǎn)單的鏈接這個(gè)到一個(gè)測(cè)試 app 已經(jīng)可以讓 [obj message] 表達(dá)式使用我們自己的代碼而不是蘋果的 runtime,這樣可以相當(dāng)方便的測(cè)試我們的代碼確保它可以工作。

整型數(shù)和指針參數(shù)會(huì)被傳入寄存器 %rsi, %rdi, %rdx, %rcx, %r8 和 %r9。其他類型的參數(shù)會(huì)被傳進(jìn)棧(stack)中。這個(gè)函數(shù)最先做的事情是把這六個(gè)寄存器中的值保存在棧中,這樣它們可以在之后被恢復(fù):

pushq %rsi

pushq %rdi

pushq %rdx

pushq %rcx

pushq %r8

pushq %r9

除了這些寄存器,寄存器 %rax 扮演了一個(gè)隱藏的參數(shù)。它用于變參的調(diào)用,并保存?zhèn)魅氲南蛄考拇嫫鳎╲ector registers)的數(shù)量,用于被調(diào)用的函數(shù)可以正確的準(zhǔn)備變參列表。以防目標(biāo)函數(shù)是個(gè)變參的方法,我同樣也保存了這個(gè)寄存器中的值:

pushq %rax

為了完整性,用于傳入浮點(diǎn)類型參數(shù)的寄存器 %xmm 也應(yīng)該被保存。但是,要是我能確保 GetImplementation 不會(huì)傳入任何的浮點(diǎn)數(shù),我就可以忽略掉它們,這樣我就可以讓代碼更簡(jiǎn)潔。

接著,對(duì)齊棧。 Mac OS X 要求一個(gè)函數(shù)調(diào)用棧需要對(duì)齊 16 字節(jié)邊界。上面的代碼已經(jīng)是棧對(duì)齊的,但是還是需要顯式手動(dòng)處理下,這樣可以確保所有都是對(duì)齊的,就不用擔(dān)心動(dòng)態(tài)調(diào)用函數(shù)時(shí)會(huì)崩潰。要對(duì)齊棧,在保存 %r12 的原始值到棧中后,我把當(dāng)前的棧指針保存到了 %r12 中。%r12 是隨便選的,任何保存的調(diào)用者寄存器(caller-saved register)都可以。重要的是在調(diào)用完 GetImplementation 后這些值仍然存在。然后我把棧指針按位與(and)上 -0x10,這樣可以清除棧底的四位:

pushq %r12

mov %rsp, %r12

andq $-0x10, %rsp

現(xiàn)在棧指針是對(duì)齊的了。這樣可以安全的避開上面(above)保存的寄存器,因?yàn)闂J窍蛳略鲩L(zhǎng)的,這種對(duì)齊的方法會(huì)讓它更向下(move it further down)。

是時(shí)候該調(diào)用 GetImplementation 了。它接收兩個(gè)參數(shù),self 和 _cmd。 調(diào)用習(xí)慣是把這兩個(gè)參數(shù)分別保存到 %rsi 和 %rdi 中。然而傳入 objc_msgSend 中時(shí)就是那樣了,它們沒有被移動(dòng)過,所以不需要改變它們。所有要做的事情實(shí)際上是調(diào)用 GetImplementation,方法名前面也要有一個(gè)下劃線:

callq _GetImplementation

整型數(shù)和指針類型的返回值保存在 %rax 中,這就是找到返回的 IMP 的地方。因?yàn)?%rax 需要被恢復(fù)到初始的狀態(tài),返回的 IMP 需要被移動(dòng)到別的地方。我隨便選了個(gè) %r11。

mov %rax, %r11

現(xiàn)在是時(shí)候該恢復(fù)原樣了。首先要恢復(fù)之前保存在 %r12 中的棧指針,然后恢復(fù)舊的 %r12 的值:

mov %r12, %rsp

popq %r12

然后按壓入棧的相反順序恢復(fù)寄存器的值:

popq %rax

popq %r9

popq %r8

popq %rcx

popq %rdx

popq %rdi

popq %rsi

現(xiàn)在一切都已經(jīng)準(zhǔn)備好了。參數(shù)寄存器(argument registers)都恢復(fù)到了之前的樣子。目標(biāo)函數(shù)需要的參數(shù)都在合適的位置了。 IMP 在寄存器 %r11 中,現(xiàn)在要做的是跳轉(zhuǎn)到那里:

jmp *%r11

就這樣!不需要其他的匯編代碼了。jump 把控制權(quán)交給了方法實(shí)現(xiàn)。從代碼的角度看,就好像發(fā)送消息者直接調(diào)用的這個(gè)方法。之前的那些迂回的調(diào)用方法都消失了。當(dāng)方法返回,它會(huì)直接放回到 objc_msgSend 的調(diào)用處,不需要其他的操作。這個(gè)方法的返回值可以在合適的地方找到。

非常規(guī)的返回值有一些細(xì)節(jié)需要注意。比如大的結(jié)構(gòu)體(不能用一個(gè)寄存器大小保存的返回值)。在 x86-64,大的結(jié)構(gòu)體使用隱藏的第一個(gè)參數(shù)返回。當(dāng)你像這樣調(diào)用:

NSRect r = SomeFunc(a, b, c);

這個(gè)調(diào)用會(huì)被翻譯成這樣:

NSRect r;

SomeFunc(&r, a, b, c);

用于返回值的內(nèi)存地址被傳入到 %rdi 中。因?yàn)?objc_msgSend 期望 %rdi 和 %rsi 中包含 self 和 _cmd,當(dāng)一個(gè)消息返回大的結(jié)構(gòu)體時(shí)不會(huì)起作用的。同樣的問題存在于多個(gè)不同平臺(tái)上。runtime 提供了 objc_msgSend_stret 用于返回結(jié)構(gòu)體,工作原理和 objc_msgSend 類似,只是知道在 %rsi 中尋找 self 和在 %rdx 中尋找 _cmd。

相似的問題發(fā)生在一些平臺(tái)上發(fā)送消息(messages)返回浮點(diǎn)類型值。在這些平臺(tái)上,runtime 提供了 objc_msgSend_fpret(在 x86-64,objc_msgSend_fpret2 用于特別極端的情況)。

方法查找

讓我們繼續(xù)實(shí)現(xiàn) GetImplementation。上面的匯編蹦床意味著這些代碼可以用 C 實(shí)現(xiàn)。記得嗎,在真正的 runtime 中,這些代碼都是直接用匯編寫的,是為了盡可能的保證最快的速度。這樣不僅可以更好的控制代碼,也可以避免重復(fù)像上面那樣保存并恢復(fù)寄存器的代碼。

GetImplementation 可以簡(jiǎn)單的調(diào)用 class_getMethodImplementation 實(shí)現(xiàn),混入 Objective-C runtime 的實(shí)現(xiàn)。這有點(diǎn)無聊。真正的 objc_msgSend 為了最大化速度首先會(huì)查找類的方法緩存。因?yàn)?GetImplementation 想模仿 objc_msgSend,所以它也會(huì)這么做。要是緩存中不包含給定的 selector 入口點(diǎn)(entry),它會(huì)繼續(xù)查找 runtime(it fall back to querying the runtime)。

我們現(xiàn)在需要的是一些結(jié)構(gòu)體定義。方法緩存是類(class)結(jié)構(gòu)體中的私有結(jié)構(gòu)體,為了得到它我們需要定義自己的版本。盡管是私有的,這些結(jié)構(gòu)體的定義還是可以通過蘋果的 Objective-C runtime 開源實(shí)現(xiàn)獲得(譯注:http://opensource.apple.com/tarballs/objc4/)。

首先需要定義一個(gè) cache entry:

typedef struct {

SEL name;

void *unused;

IMP imp;

} cache_entry;

相當(dāng)簡(jiǎn)單。別問我 unused 字段是干什么的,我也不知道它為什么在那。這是 cache 的全部定義:

struct objc_cache {

uintptr_t mask;

uintptr_t occupied;

cache_entry *buckets[1];

};

緩存使用 hash table(哈希表)實(shí)現(xiàn)。實(shí)現(xiàn)這個(gè)表是為了速度的考慮,其他無關(guān)的都簡(jiǎn)化了,所以它有點(diǎn)不一樣。表的大小永遠(yuǎn)都是 2 的冪。表格使用 selector 做索引,bucket 是直接使用 selector 的值做索引,可能會(huì)通過移位去除不相關(guān)的低位(low bits),并與 mask 執(zhí)行一個(gè)邏輯與(logical and)。下面是一些宏,用于給定 selector 和 mask 時(shí)計(jì)算 bucket 的索引:

#ifndef __LP64__

# define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))

#else

# define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>0)) & (mask))

#endif

最后是類的結(jié)構(gòu)體。 這是 Class 指向的類型:

struct class_t {

struct class_t *isa;

struct class_t *superclass;

struct objc_cache *cache;

IMP *vtable;

};

需要的結(jié)構(gòu)體都已經(jīng)有了,現(xiàn)在開始實(shí)現(xiàn) GetImplementation 吧:

IMP GetImplementation(id self, SEL _cmd)

{

首先要做的是獲取對(duì)象的類。真正的 objc_msgSend 通過類似 self->isa 的方式獲取,但是它會(huì)使用官方的 API 實(shí)現(xiàn):

Class c = object_getClass(self);

因?yàn)槲蚁朐L問最原始的形式,我會(huì)為指向 class_t 結(jié)構(gòu)體的指針執(zhí)行類型轉(zhuǎn)換:

struct class_t *classInternals = (struct class_t *)c;

現(xiàn)在該查找 IMP 了。首先我們把它初始為 NULL。如果我們?cè)诰彺嬷姓业?,我們?huì)賦值為它。如果查找緩存后仍為 NULL,我們會(huì)回退到速度較慢的方法:

IMP imp = NULL;

接著,獲取指向 cache 的指針:

struct objc_cache *cache = classInternals->cache;

計(jì)算 bucket 的索引,獲取指向 buckets 數(shù)組的指針:

uintptr_t index = CACHE_HASH(_cmd, cache->mask);

cache_entry **buckets = cache->buckets;

然后,我們使用要找的 selector 查找緩存。runtime 使用的是線性鏈(linear chaining),之后只是遍歷 buckets 子集直到找到需要的 entry 或者 NULL entry:

for(; buckets[index] != NULL; index = (index + 1) & cache->mask)

{

if(buckets[index]->name == _cmd)

{

imp = buckets[index]->imp;

break;

}

}

如果沒有找到 entry,我們會(huì)調(diào)用 runtime 使用一種較慢的方法。在真正的 objc_msgSend 中,上面的所有代碼都是使用匯編實(shí)現(xiàn)的,這時(shí)候就該離開匯編代碼調(diào)用 runtime 自己的方法了。一旦查找緩存后沒有找到需要的 entry,期望快速發(fā)送消息的希望就要落空了。這時(shí)候獲取更快的速度就沒那么重要了,因?yàn)橐呀?jīng)注定會(huì)變慢,在一定程度上也極少的需要這么調(diào)用。因?yàn)檫@點(diǎn),放棄匯編代碼轉(zhuǎn)而使用更可維護(hù)的 C 也是可以接受的:

if(imp == NULL)

imp = class_getMethodImplementation(c, _cmd);

不管怎樣,IMP 現(xiàn)在已經(jīng)獲取到了。如果它在緩存中,就會(huì)在那里找到它,否則它會(huì)通過 runtime 查找到。class_getMethodImplementation 調(diào)用同樣會(huì)使用緩存,所以下次調(diào)用會(huì)更快。剩下的就是返回 IMP:

return imp;

}

測(cè)試

為了確保它能工作,我寫了一個(gè)快速的測(cè)試程序:

@interface Test : NSObject

- (void)none;

- (void)param: (int)x;

- (void)params: (int)a : (int)b : (int)c : (int)d : (int)e : (int)f : (int)g;

- (int)retval;

@end

@implementation Test

- (id)init

{

fprintf(stderr, "in init method, self is %p\n", self);

return self;

}

- (void)none

{

fprintf(stderr, "in none method\n");

}

- (void)param: (int)x

{

fprintf(stderr, "got parameter %d\n", x);

}

- (void)params: (int)a : (int)b : (int)c : (int)d : (int)e : (int)f : (int)g

{

fprintf(stderr, "got params %d %d %d %d %d %d %d\n", a, b, c, d, e, f, g);

}

- (int)retval

{

fprintf(stderr, "in retval method\n");

return 42;

}

@end

int main(int argc, char **argv)

{

for(int i = 0; i < 20; i++)

{

Test *t = [[Test alloc] init];

[t none];

[t param: 9999];

[t params: 1 : 2 : 3 : 4 : 5 : 6 : 7];

fprintf(stderr, "retval gave us %d\n", [t retval]);

NSMutableArray *a = [[NSMutableArray alloc] init];

[a addObject: @1];

[a addObject: @{ @"foo" : @"bar" }];

[a addObject: @("blah")];

a[0] = @2;

NSLog(@"%@", a);

}

}

以防因?yàn)橐恍┮馔庹{(diào)用的是 runtime 的實(shí)現(xiàn)。我在 GetImplementation 中加了一些調(diào)試的日志確保它被調(diào)用了。一切都正常,即使是 literals and subscripting 也都調(diào)用的是替換的實(shí)現(xiàn)。

結(jié)論

objc_msgSend 的核心部分相當(dāng)?shù)暮?jiǎn)單。但它的實(shí)現(xiàn)需要一些匯編代碼,這讓它比它應(yīng)該的樣子更難理解。但是為了性能的優(yōu)化還是得使用一些匯編代碼。但是通過構(gòu)建了一個(gè)簡(jiǎn)單的匯編蹦床,然后使用 C 實(shí)現(xiàn)了它的邏輯,我們可以看到它是如何工作的,它真的沒有什么高深的。

很顯然,你不應(yīng)該在自己的 app 中使用替換的 objc_msgSend 實(shí)現(xiàn)。你會(huì)后悔這么做的。這么做只為了學(xué)習(xí)目的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容