原文連接: Why objc_msgSend Must be Written in Assembly數組
老是看到有人說用匯編實現objc_msgSend是爲了速度快,固然這個不能否認。可是難道沒有別的緣由?因而就看到了這篇文章,遂翻譯之!=。=緩存
我本身的理解就是,用匯編實現,是爲了應對不一樣的「Calling convention」,把函數調用前的棧和寄存器的參數、狀態設置,交給編譯器去處理。函數
先看看原文吧。翻譯
開始指針
對於Objective-C來講,調用一個對象實例的方法,也叫做向這個對象實例「發送消息」,而每條「消息」,在編譯階段都會轉變爲一次對objc_msgSend函數的調用,調用的參數不只有本來消息的全部參數,還有消息的接收者receiver和對應的方法selector。舉個例子,下面的語句:對象
[receiver message:foo beforeDate:bar];blog
將會被編譯成:繼承
objc_msgSend(receiver, @selector(message:beforeDate:), foo, bar);ip
對於objc_msgSend函數的實現原理,前人已經作了大量的探索。因此,本文將會把重點放在objc_msgSend的一個以前沒有太受到關注的點上,那就是:get
objc_msgSend是不可能用Objective-C、C或者C++實現的。
THE RETURN TYPE - 返回類型
先看看以下兩行代碼:
NSUInteger n = [array count];
id obj = [array objectAtIndex:6];
直觀上看,將會被編譯成:
NSUInteger n = objc_msgSend(array, @selector(count));
id obj = objc_msgSend(array, @selector(objectAtIndex:), 6);
可是實際上這是不可能的,由於沒有函數能夠同時知足這兩個調用。並且它的返回值也不能同時是NSUInteger和id。
並且,上面的代碼也是沒法編譯經過的。那麼,加上類型轉換怎麼樣?
NSUInteger n = (NSUInteger (*)(id, SEL))objc_msgSend(array, @selector(count));
id obj = (id (*)(id, SEL, NSUInteger))objc_msgSend(array, @selector(objectAtIndex:), 6);
這下能夠編譯經過了,雖然看起來不直觀。。。
objc_msgSend是一個Public的函數,在<objc/message.h>裏聲明,若是你想直接調用它,就必須按照上面的格式加上強制類型轉換,要否則是沒法編譯經過的。可是objc_msgSend究竟是如何實現,來支持各類返回類型的?本文後面會講到。
THE IMP - 方法對應的函數指針
objc_msgSend函數的本質很簡單,傳入一個接受者對象實例receiver和方法名selector,它就會按照如下步驟執行:(譯者注:只是最粗略的步驟=。=)
整個流程很簡單,沿着繼承鏈,向上找到方法selector對應的函數指針便可,也就是IMP。同時,在每層Class中都有緩存,加快後續的方法查找。可是,這也只是objc_msgSend的實現細節,因此,接着往下看。
THE ARG TYPES AND COUNT - 參數類型和數量
簡單來講,當objc_msgSend找到對應的函數指針後,只要用傳入的參數調用這個函數便可。剩下來的就是找到一種方法,能夠調用任意參數類型、數量的任意函數。
參數的數量很容易計算。而後咱們能夠把全部的參數都放入varargs,而後調用函數時傳入便可。可是這樣的話,每一個Objective-C的方法都必須在其prologue(譯者注:函數執行具體的「任務」前,所作的準備環節)裏面把全部的參數從varargs裏面提取出來。
這種把參數打包到varargs裏面而後又取出來的辦法顯然是很是糟糕的,同時也是沒必要要的。
在C語言中,調用一個函數會被編譯成對應的彙編語言指令,首先是設置參數(把參數放到寄存器、棧上),而後用如jump或者call的指令,跳到具體的函數代碼地址處。若是咱們想支持任意類型的函數類型,咱們就必須寫一個switch語句,把全部的參數組合狀況都包含起來,這樣才能正確的爲任何形式的函數設置參數(譯者注:即按照某種「規範」、「約定」,把參數依次存放到「約定」的寄存器、棧上),這顯然是沒有擴展性的,更是不可能的。
UNWINDING THE CALL - 拆解調用
objc_msgSend的解決辦法,主要依據的是:當objc_msgSend被調用時,全部的參數已經被設置好了。
換一種方式來講,就是:在objc_msgSend開始執行時,棧幀(stack frame)的狀態、數據,和各個寄存器的組合形式、數據,跟調用具體的函數指針(IMP)時所需的狀態、數據,是徹底一致的!
以下這行代碼:
id obj = objc_msgSend(array, @selector(objectAtIndex:), 6);
在調用objc_msgSend時,須要設置三個參數,分別是被調用方receiver、方法名selector和最後一個整型參數6。這和具體的方法函數IMP的參數順序、類型是徹底一致的,也就是說,調用objc_msgSend前,設置的棧、寄存器的狀態、數據正是調用具體的方法函數時須要的狀態!
因此,當objc_msgSend找到要調用的函數實現IMP後,只須要把全部的對棧、寄存器的操做「倒」回到objc_msgSend執行開始的狀態(相似於函數執行完成return返回前,作的「收尾處理」工做同樣,即epilogue),直接jump/call到IMP函數指針對應的地址,執行指令便可,由於全部的參數已經被設置好了。
同時,當selector對應的IMP執行完成後,返回值也被正確的設置好了(在x86平臺上,返回值被設置到了指定的寄存器eax/rax裏,在arm上,則是r0寄存器),因此,咱們也沒必要擔憂前文提到的不一樣類型的返回值問題了。
WRAP UP - 總結
把上面提到的全部解釋綜合起來,就是:在C語言裏面調用函數,必須在編譯時就知道調用的「狀態」;而這些「狀態」在運行時是沒法得出或正確處理的,因此必須往底層走,用匯編處理。