初步理解objc_msgSend

Objective-c方法調用本質上是消息傳遞。消息包括消息名稱name,選擇器selector(其實就是函數指針)。傳遞的消息能夠接受參數,也可能有返回值。緩存

C語言的函數調用

要理解OC的消息傳遞,就該說一下C語言的函數調用方式,畢竟OC是C語言的延伸語言。C語言使用靜態綁定Static binding進行函數調用,說人話就是C語言在編譯期就能決定運行時要調用的函數。好比:架構

#import <stdio.h>

void printHello() {
    printf("Hello,world!\n");
}

void printGoodbye() {
    printf("Goodbye,world!\n");
}

void doSomeThing(int type) {
    if (type == 0) {
        printHello();
    }else {
        printGoodbye();
    }
    return 0;
}
複製代碼

若是不考慮內聯函數的狀況(C語言中爲提升函數調用的效率,直接將函數體對函數調用進行替換的方式),那麼在C語言中,編譯器在編譯期就已經知道程序中有printHello和printGoodbye這兩個函數了,因而就能夠直接調用這兩個函數的指令,實際上這兩個函數的地址是被硬編碼在指令裏面的。函數

咱們把上述例子改變一下:性能

#import <stdio.h>

void printHello() {
    printf("Hello,world!\n");
}

void printGoodbye() {
    printf("Goodbye,world!\n");
}

void doSomeThing(int type) {
    void (*fnc)();
    if (type == 0) {
        fnc = printHello;
    }else {
        fnc = printGoodbye;
    }
    fnc();
    return 0;
}
複製代碼

這時候就使用了動態綁定Dynamic binding,所調用的函數要到運行期才能肯定。在第二個例子的代碼中,只有一個函數調用指令,而且待調用函數的函數地址並不能硬編碼在指令中,而是要等到運行期才能讀取。優化

Objective-c的消息傳遞

在OC中,傳遞消息其實就是用動態綁定機制來決定須要調用的方法。在OC底層,全部的方法都是普通的C語言函數,對象收到消息後,調用哪一個方法徹底由運行時決定,甚至在程序運行的時候發生改變,因此OC是一門動態語言。ui

一個典型的方法調用或者說給對象發送消息能夠這樣來寫:編碼

id returnValue = [someObj messageName:parameter];
複製代碼

someObj叫作接收者messageName選擇器,選擇器及其參數,一塊兒被稱爲消息(Message)。編譯器看到這條消息後,會把它轉化爲一條標準的C語言函數調用,這個是整個OC運行時消息傳遞機制的核心,也就是objc_msgSend,原型以下:spa

void objc_msgSend(id self,SEL cmd,...) 複製代碼

objc_msgSend是一個參數可變函數,能接受兩個或兩個以上參數。第一個參數是接受者,第二個參數是選擇器,其中SEL爲選擇器的類型,後面就是消息中的參數了,順序不變。因此把剛纔那個典型的方法調用轉換以下:指針

id returnValue = objc_msgSend(someObj,
                                @selector(messageName:),
                                parameter);
複製代碼

objc_msgSend函數會依據接受者和選擇器的類型調用適當的方法。此時,objc_msgSend會在接收者所屬的類中搜尋一個方法列表,若是能找到和選擇器名稱相符合的方法,就跳轉到其實現的代碼。若是找不到,就沿着繼承體系繼續向上查找,等找到合適的方法後再跳轉。若是這時候仍是找不到符合的方法,就執行消息轉發(message forwarding)操做,這個會在下一篇文章中詳解。下面用一個流程圖說明上述過程:code

在整個過程當中,objc_msgSend會將匹配結果緩存在一個快速映射表裏,每個類都有一塊這樣的緩存。以後同一個類發送相同選擇器的消息時,執行起來就會更快。但實際上,這種方式的速度仍是不如靜態綁定那麼快,但大多數狀況下,消息發送並非一個應用程序的性能瓶頸,若是真的出現了性能問題,能夠編寫單純的C語言函數,在調用時根據須要,傳入OC對象便可。

邊界狀況

一些邊界狀況,OC的運行環境會提供另一些對應的函數處理:

  • objc_msgSend_stret:處理待發送消息須要返回結構體的狀況,但當結構體過大,致使CPU寄存器沒法容納時,會把消息交給另外一個函數派發,把返回的機構體經過分配在棧上的某個變量處理。
  • objc_msgSend_fpret:處理待發送消息須要返回浮點數的狀況。在某些架構的CPU中,須要對浮點寄存器作一些特殊處理,因此這時候調用objc_msgSend並不合適。
  • objc_msgSendSuper:向父類發送消息。

objc_msgSend找到對應的方法調用實現後,會直接跳轉過去,OC中每個方法均可以理解爲是一個簡單的C語言函數,原型以下:

<return_type> Class_selecotor(id self,SEL _cmd,...)
複製代碼

每一個類的方法列表中的指針都會指向這種函數,選擇器就是查表時候所用的「鍵」。

這裏咱們發現,方法的原型模樣和objc_msgSend有些相似,這不是巧合,而是OC利用了尾調用優化

尾調用優化

若是某個函數最後一項操做也是調用另外一個函數,那麼就能夠利用尾調用優化了。編譯器會生成跳轉到另外一個函數所須要的指令碼,並且不會向調用棧中推入新的棧幀。這裏有幾個須要解釋的地方:

  • OC的方法調用會爲objc_msgSend準備棧幀,是一個進棧的過程。
  • 當某個函數最後一項操做也是調用另外一個函數,而且調用的函數不做爲返回值另作他用,才能使用尾調用優化。

    好比最後返回return [self message:someMsg];時能夠進行尾調用優化,但return [self message:someMsg]+1;這種狀況不行,由於雖然調用了函數,但最終調用的函數是爲返回值作準備的。

  • 尾調用優化會經過複用棧幀,避免調用方法時不斷的進棧形成棧溢出最後程序崩潰。
相關文章
相關標籤/搜索