objc_msgSend 函數支撐了我們使用 Objective-C 實現的一切。Gwynne Raskind,Friday Q&A 的讀者,建議我談談 objc_msgSend 的內部實現。要理解某件事還有比自己動手實現一次更好的方法嗎?咱們來自己動手實現一個 objc_msgSend。
Tramapoline! Trampopoline! (蹦床)
當你寫了一個發送 Objective-C 消息的方法:
[obj message]
編譯器會生成一個 objc_msgSend 調用:
objc_msgSend(obj, @selector(message));
之后 objc_msgSend 會負責轉發這個消息。
它都做了什么?它會查找合適的函數指針或者 IMP,然后調用,最后跳轉。任何傳給 objc_msgSend 的參數,最終都會成為 IMP 的參數。 IMP 的返回值成為了最開始被調用的方法的返回值。
因為 objcmsgSend 只是負責接收參數,找到合適的函數指針,然后跳轉,有時管這種叫做 trampoline(譯注:[蹦床](https://en.wikipedia.org/wiki/Trampoline(computing)). 更通用的來說,任何一段負責把一段代碼轉發到另一處的代碼,都可以被叫做 trampoline。
這種轉發的行為使 objc_msgSend 變得特殊起來。因為它只是簡單的查找合適的代碼,然后直接跳轉過去,這相當的通用。傳入任何參數組合都可以,因為它只是把這些參數留給 IMP 去讀取。返回值有些棘手,但最終都可以看成 objc_msgSend 的不同變種。
不幸的是,這些轉發行為都不能用純 C 實現。因為沒有方法可以將傳入 C 函數的泛參(generic parameters)傳給另一個函數。 你可以使用變參,但是變參和普通參數的傳遞方法不同,而且慢,所以這不適合普通的 C 參數。
如果要用 C 來實現 objc_msgSend,基本樣子應該像這樣:
id objc_msgSend(id self, SEL _cmd, ...)
{
Class c = object_getClass(self);
IMP imp = class_getMethodImplementation(c, _cmd);
return imp(self, _cmd, ...);
}
這有點過于簡單。事實上會有一個方法緩存來提升查找速度,像這樣:
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 函數實現。
匯編
在 Apple 版的 runtime 中,為了最大化速度,整個函數是使用匯編實現的。在 Objective-C 中每次發送消息都會調用 objc_msgSend,在一個應用中最簡單的動作都會有成千或者上百萬的消息。
為了讓事情更簡單,我自己的實現中會盡可能少的使用匯編,使用獨立的 C 函數抽象復雜度。匯編代碼會實現下面的功能:
id objc_msgSend(id self, SEL _cmd, ...)
{
IMP imp = GetImplementation(self, _cmd);
imp(self, _cmd, ...);
}
GetImplementation 可以用更可讀的方式工作。
匯編代碼需要:
1. 把所有潛在的參數存儲在安全的地方,確保 GetImplementation 不會覆蓋它們。
2. 調用 GetImplementation。
3. 把返回值保存在某處。
4. 恢復所有的參數值。
5. 跳轉到 GetImplementation 返回的 IMP。
讓我們開始吧!
這里我會嘗試使用 x86-64 匯編,這樣可以很方便的在 Mac 上工作。這些概念也可以應用于 i386 或者 ARM。
這個函數會保存在獨立的文件中,叫做 msgsend-asm.s。這個文件可以像源文件那樣傳遞給編譯器,然后會被編譯并鏈接到程序中。
新聞熱點
疑難解答