iOS 理解objc_msgSend的作用

weixin_33670713發表於2018-01-26

好記性不如爛筆頭,這塊偏硬的概念還是自己打一遍理解下好

在物件上呼叫方法是Objective-C中經常使用的功能。用Objective-C的術語來說,這叫做“傳遞訊息”(pass a message)。訊息有“名稱”(name)或“選擇子”(selector),可以接受引數,而且可能還有返回值。
由於Objective-C是C的超集,所以最好先理解C語言的函式呼叫方式。C語言使用“靜態繫結”(static binding),也就是說,在編譯期就能決定執行時所應呼叫的函式。以下列程式碼為例:

#import <stdio.h> 
 
void printHello() {  
    printf("Hello, world!\n");  
}  
void printGoodbye() {  
    printf("Goodbye, world!\n");  
}  
 
void doTheThing(int type) {  
    if (type == 0) {  
        printHello();  
    } else {  
        printGoodbye();  
    }  
    return 0;  
} 

如果不考慮“內聯”(inline),那麼編譯器在編譯程式碼的時候就已經知道程式中有printHello與printGoodbye這兩個函式了,於是會直接生成呼叫這些函式的指令。而函式地址實際上是硬編碼在指令之中的。若是將剛才那段程式碼寫成下面這樣,會如何呢?

#import <stdio.h> 
 
void printHello() {  
    printf("Hello, world!\n");  
}  
void printGoodbye() {  
    printf("Goodbye, world!\n");  
}  
 
void doTheThing(int type) {  
    void (*fnc)();  
    if (type == 0) {  
        fnc = printHello;  
    } else {  
        fnc = printGoodbye;  
    }  
    fnc();  
    return 0;  
} 
這

時就得使用“動態繫結”(dynamic binding)了,因為所要呼叫的函式直到執行期才能確定。編譯器在這種情況下生成的指令與剛才那個例子不同,在第一個例子中,if與else語句裡都有函式呼叫指令。而在第二個例子中,只有一個函式呼叫指令,不過待呼叫的函式地址無法硬編碼在指令之中,而是要在執行期讀取出來。
在Objective-C中,如果向某物件傳遞訊息,那就會使用動態繫結機制來決定需要呼叫的方法。在底層,所有方法都是普通的C語言函式,然而物件收到訊息之後,究竟該呼叫哪個方法則完全於執行期決定,甚至可以在程式執行時改變,這些特性使得Objective-C成為一門真正的動態語言。
給物件傳送訊息可以這樣來寫:
id returnValue = [someObject messageName:parameter];
在本例中,someObject叫做“接收者”(receiver),messageName叫做“選擇子”(selector)。選擇子與引數合起來稱為“訊息”(message)。編譯器看到此訊息後,將其轉換為一條標準的C語言函式呼叫,所呼叫的函式乃是訊息傳遞機制中的核心函式,叫做objc_msgSend,其“原型”(prototype)如下:
void objc_msgSend(id self, SEL cmd, ...)
這是個“引數個數可變的函式”(variadic function),能接受兩個或兩個以上的引數。第一個引數代表接收者,第二個引數代表選擇子(SEL是選擇子的型別),後續引數就是訊息中的那些引數,其順序不變。選擇子指的就是方法的名字。“選擇子”與“方法”這兩個詞經常交替使用。編譯器會把剛才那個例子中的訊息轉換為如下函式:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend函式會依據接收者與選擇子的型別來呼叫適當的方法。為了完成此操作,該方法需要在接收者所屬的類中搜尋其“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法,就跳至其實現程式碼。若是找不到,那就沿著繼承體系繼續向上查詢,等找到合適的方法之後再跳轉。如果最終還是找不到相符的方法,那就執行“訊息轉發”(message forwarding)操作。訊息轉發將在第12條中詳解。
這麼說來,想呼叫一個方法似乎需要很多步驟。所幸objc_msgSend會將匹配結果快取在“快速對映表”(fast map)裡面,每個類都有這樣一塊快取,若是稍後還向該類傳送與選擇子相同的訊息,那麼執行起來就很快了。當然啦,這種“快速執行路徑”(fast path)還是不如“靜態繫結的函式呼叫操作”(statically bound function call)那樣迅速,不過只要把選擇子快取起來了,也就不會慢很多,實際上,訊息派發(message dispatch)並非應用程式的瓶頸所在。假如真是個瓶頸的話,那你可以只編寫純C函式,在呼叫時根據需要,把Objective-C物件的狀態傳進去。
前面講的這部分內容只描述了部分訊息的呼叫過程,其他“邊界情況”(edge case)則需要交由Objective-C執行環境中的另一些函式來處理:
objc_msgSend_stret。如果待傳送的訊息要返回結構體,那麼可交由此函式處理。只有當CPU的暫存器能夠容納得下訊息返回型別時,這個函式才能處理此訊息。若是返回值無法容納於CPU暫存器中(比如說返回的結構體太大了),那麼就由另一個函式執行派發。此時,那個函式會通過分配在棧上的某個變數來處理訊息所返回的結構體。
objc_msgSend_fpret。如果訊息返回的是浮點數,那麼可交由此函式處理。在某些架構的CPU中呼叫函式時,需要對“浮點數暫存器”(floating-point register)做特殊處理,也就是說,通常所用的objc_msgSend在這種情況下並不合適。這個函式是為了處理x86等架構CPU中某些令人稍覺驚訝的奇怪狀況。
objc_msgSendSuper。如果要給超類發訊息,例如[super message:parameter],那麼就交由此函式處理。也有另外兩個與objc_msgSend_stret和objc_msgSend_fpret等效的函式,用於處理髮給super的相應訊息。
剛才曾提到,objc_msgSend等函式一旦找到應該呼叫的方法實現之後,就會“跳轉過去”。之所以能這樣做,是因為Objective-C物件的每個方法都可以視為簡單的C函式,其原型如下:
<return_type> Class_selector(id self, SEL _cmd, ...)
真正的函式名和上面寫的可能不太一樣,筆者用“類”(class)和“選擇子”(selector)來命名是想解釋其工作原理。每個類裡都有一張表格,其中的指標都會指向這種函式,而選擇子的名稱則是查表時所用的“鍵”。objc_msgSend等函式正是通過這張表格來尋找應該執行的方法並跳至其實現的。請注意,原型的樣子和objc_msgSend函式很像。這不是巧合,而是為了利用“尾呼叫優化”(tail-call optimization)技術,令“跳至方法實現”這一操作變得更簡單些。
如果某函式的最後一項操作是呼叫另外一個函式,那麼就可以運用“尾呼叫優化”技術。編譯器會生成調轉至另一函式所需的指令碼,而且不會向呼叫堆疊中推入新的“棧幀”(frame stack)。只有當某函式的最後一個操作僅僅是呼叫其他函式而不會將其返回值另作他用時,才能執行“尾呼叫優化”。這項優化對objc_msgSend非常關鍵,如果不這麼做的話,那麼每次呼叫Objective-C方法之前,都需要為呼叫objc_msgSend函式準備“棧幀”,大家在“棧蹤跡”(stack trace)中可以看到這種“棧幀”。此外,若是不優化,還會過早地發生“棧溢位”(stack overflow)現象。
在實際編寫Objective-C程式碼的過程中,大家無須擔心這一問題,不過應該瞭解其底層工作原理。這樣的話,你就會明白,在傳送訊息時,程式碼究竟是如何執行的,而且也能理解,為何在除錯的時候,棧“回溯”(backtrace)資訊中總是出現objc_msgSend。

要點

  • 訊息由接收者、選擇子及引數構成。給某物件“傳送訊息”(invoke a message)也就相當於在該物件上“呼叫方法”(call a method)。
  • 發給某物件的全部訊息都要由“動態訊息派發系統”(dynamic message dispatch system)來處理,該系統會查出對應的方法,並執行其程式碼。

讀Effective Objective-C 2.0 有感

相關文章