爲何objc_msgSend必須用匯編實現

原文連接: 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,它就會按照如下步驟執行:(譯者注:只是最粗略的步驟=。=)

  • 獲取receiver得類Class
  • 在Class的方法列表method table裏面查找對應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語言裏面調用函數,必須在編譯時就知道調用的「狀態」;而這些「狀態」在運行時是沒法得出或正確處理的,因此必須往底層走,用匯編處理。

相關文章
相關標籤/搜索