好記性不如爛筆頭,這塊偏硬的概念還是自己打一遍理解下好
在對象上調(diào)用方法是Objective-C中經(jīng)常使用的功能绅这。用Objective-C的術(shù)語來說,這叫做“傳遞消息”(pass a message)在辆。消息有“名稱”(name)或“選擇子”(selector)证薇,可以接受參數(shù),而且可能還有返回值匆篓。
由于Objective-C是C的超集浑度,所以最好先理解C語言的函數(shù)調(diào)用方式。C語言使用“靜態(tài)綁定”(static binding)鸦概,也就是說箩张,在編譯期就能決定運(yùn)行時(shí)所應(yīng)調(diào)用的函數(shù)。以下列代碼為例:
#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;
}
如果不考慮“內(nèi)聯(lián)”(inline)窗市,那么編譯器在編譯代碼的時(shí)候就已經(jīng)知道程序中有printHello與printGoodbye這兩個(gè)函數(shù)了先慷,于是會直接生成調(diào)用這些函數(shù)的指令。而函數(shù)地址實(shí)際上是硬編碼在指令之中的咨察。若是將剛才那段代碼寫成下面這樣论熙,會如何呢?
#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;
}
這
時(shí)就得使用“動態(tài)綁定”(dynamic binding)了摄狱,因?yàn)樗{(diào)用的函數(shù)直到運(yùn)行期才能確定脓诡。編譯器在這種情況下生成的指令與剛才那個(gè)例子不同,在第一個(gè)例子中媒役,if與else語句里都有函數(shù)調(diào)用指令祝谚。而在第二個(gè)例子中,只有一個(gè)函數(shù)調(diào)用指令刊愚,不過待調(diào)用的函數(shù)地址無法硬編碼在指令之中踊跟,而是要在運(yùn)行期讀取出來。
在Objective-C中,如果向某對象傳遞消息商玫,那就會使用動態(tài)綁定機(jī)制來決定需要調(diào)用的方法箕憾。在底層,所有方法都是普通的C語言函數(shù)拳昌,然而對象收到消息之后袭异,究竟該調(diào)用哪個(gè)方法則完全于運(yùn)行期決定,甚至可以在程序運(yùn)行時(shí)改變炬藤,這些特性使得Objective-C成為一門真正的動態(tài)語言御铃。
給對象發(fā)送消息可以這樣來寫:
id returnValue = [someObject messageName:parameter];
在本例中,someObject叫做“接收者”(receiver)沈矿,messageName叫做“選擇子”(selector)上真。選擇子與參數(shù)合起來稱為“消息”(message)。編譯器看到此消息后羹膳,將其轉(zhuǎn)換為一條標(biāo)準(zhǔn)的C語言函數(shù)調(diào)用睡互,所調(diào)用的函數(shù)乃是消息傳遞機(jī)制中的核心函數(shù),叫做objc_msgSend陵像,其“原型”(prototype)如下:
void objc_msgSend(id self, SEL cmd, ...)
這是個(gè)“參數(shù)個(gè)數(shù)可變的函數(shù)”(variadic function)就珠,能接受兩個(gè)或兩個(gè)以上的參數(shù)。第一個(gè)參數(shù)代表接收者醒颖,第二個(gè)參數(shù)代表選擇子(SEL是選擇子的類型)妻怎,后續(xù)參數(shù)就是消息中的那些參數(shù),其順序不變泞歉。選擇子指的就是方法的名字逼侦。“選擇子”與“方法”這兩個(gè)詞經(jīng)常交替使用疏日。編譯器會把剛才那個(gè)例子中的消息轉(zhuǎn)換為如下函數(shù):
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend函數(shù)會依據(jù)接收者與選擇子的類型來調(diào)用適當(dāng)?shù)姆椒ǔソ唷榱送瓿纱瞬僮魅龊海摲椒ㄐ枰诮邮照咚鶎俚念愔兴褜て洹胺椒斜怼保╨ist of methods)沟优,如果能找到與選擇子名稱相符的方法,就跳至其實(shí)現(xiàn)代碼睬辐。若是找不到挠阁,那就沿著繼承體系繼續(xù)向上查找,等找到合適的方法之后再跳轉(zhuǎn)溯饵。如果最終還是找不到相符的方法侵俗,那就執(zhí)行“消息轉(zhuǎn)發(fā)”(message forwarding)操作。消息轉(zhuǎn)發(fā)將在第12條中詳解丰刊。
這么說來隘谣,想調(diào)用一個(gè)方法似乎需要很多步驟。所幸objc_msgSend會將匹配結(jié)果緩存在“快速映射表”(fast map)里面,每個(gè)類都有這樣一塊緩存寻歧,若是稍后還向該類發(fā)送與選擇子相同的消息掌栅,那么執(zhí)行起來就很快了。當(dāng)然啦码泛,這種“快速執(zhí)行路徑”(fast path)還是不如“靜態(tài)綁定的函數(shù)調(diào)用操作”(statically bound function call)那樣迅速猾封,不過只要把選擇子緩存起來了,也就不會慢很多噪珊,實(shí)際上晌缘,消息派發(fā)(message dispatch)并非應(yīng)用程序的瓶頸所在。假如真是個(gè)瓶頸的話痢站,那你可以只編寫純C函數(shù)磷箕,在調(diào)用時(shí)根據(jù)需要,把Objective-C對象的狀態(tài)傳進(jìn)去阵难。
前面講的這部分內(nèi)容只描述了部分消息的調(diào)用過程搀捷,其他“邊界情況”(edge case)則需要交由Objective-C運(yùn)行環(huán)境中的另一些函數(shù)來處理:
objc_msgSend_stret。如果待發(fā)送的消息要返回結(jié)構(gòu)體多望,那么可交由此函數(shù)處理嫩舟。只有當(dāng)CPU的寄存器能夠容納得下消息返回類型時(shí),這個(gè)函數(shù)才能處理此消息怀偷。若是返回值無法容納于CPU寄存器中(比如說返回的結(jié)構(gòu)體太大了)家厌,那么就由另一個(gè)函數(shù)執(zhí)行派發(fā)。此時(shí)椎工,那個(gè)函數(shù)會通過分配在棧上的某個(gè)變量來處理消息所返回的結(jié)構(gòu)體饭于。
objc_msgSend_fpret。如果消息返回的是浮點(diǎn)數(shù)维蒙,那么可交由此函數(shù)處理掰吕。在某些架構(gòu)的CPU中調(diào)用函數(shù)時(shí),需要對“浮點(diǎn)數(shù)寄存器”(floating-point register)做特殊處理颅痊,也就是說殖熟,通常所用的objc_msgSend在這種情況下并不合適。這個(gè)函數(shù)是為了處理x86等架構(gòu)CPU中某些令人稍覺驚訝的奇怪狀況斑响。
objc_msgSendSuper菱属。如果要給超類發(fā)消息,例如[super message:parameter]舰罚,那么就交由此函數(shù)處理纽门。也有另外兩個(gè)與objc_msgSend_stret和objc_msgSend_fpret等效的函數(shù),用于處理發(fā)給super的相應(yīng)消息营罢。
剛才曾提到赏陵,objc_msgSend等函數(shù)一旦找到應(yīng)該調(diào)用的方法實(shí)現(xiàn)之后,就會“跳轉(zhuǎn)過去”。之所以能這樣做蝙搔,是因?yàn)镺bjective-C對象的每個(gè)方法都可以視為簡單的C函數(shù)候醒,其原型如下:
<return_type> Class_selector(id self, SEL _cmd, ...)
真正的函數(shù)名和上面寫的可能不太一樣,筆者用“類”(class)和“選擇子”(selector)來命名是想解釋其工作原理杂瘸。每個(gè)類里都有一張表格倒淫,其中的指針都會指向這種函數(shù),而選擇子的名稱則是查表時(shí)所用的“鍵”败玉。objc_msgSend等函數(shù)正是通過這張表格來尋找應(yīng)該執(zhí)行的方法并跳至其實(shí)現(xiàn)的敌土。請注意,原型的樣子和objc_msgSend函數(shù)很像运翼。這不是巧合返干,而是為了利用“尾調(diào)用優(yōu)化”(tail-call optimization)技術(shù),令“跳至方法實(shí)現(xiàn)”這一操作變得更簡單些血淌。
如果某函數(shù)的最后一項(xiàng)操作是調(diào)用另外一個(gè)函數(shù)矩欠,那么就可以運(yùn)用“尾調(diào)用優(yōu)化”技術(shù)。編譯器會生成調(diào)轉(zhuǎn)至另一函數(shù)所需的指令碼悠夯,而且不會向調(diào)用堆棧中推入新的“棧幀”(frame stack)癌淮。只有當(dāng)某函數(shù)的最后一個(gè)操作僅僅是調(diào)用其他函數(shù)而不會將其返回值另作他用時(shí),才能執(zhí)行“尾調(diào)用優(yōu)化”沦补。這項(xiàng)優(yōu)化對objc_msgSend非常關(guān)鍵乳蓄,如果不這么做的話,那么每次調(diào)用Objective-C方法之前夕膀,都需要為調(diào)用objc_msgSend函數(shù)準(zhǔn)備“棧幀”虚倒,大家在“棧蹤跡”(stack trace)中可以看到這種“棧幀”。此外产舞,若是不優(yōu)化魂奥,還會過早地發(fā)生“棧溢出”(stack overflow)現(xiàn)象。
在實(shí)際編寫Objective-C代碼的過程中易猫,大家無須擔(dān)心這一問題耻煤,不過應(yīng)該了解其底層工作原理。這樣的話擦囊,你就會明白违霞,在發(fā)送消息時(shí),代碼究竟是如何執(zhí)行的瞬场,而且也能理解,為何在調(diào)試的時(shí)候涧郊,椆岜唬“回溯”(backtrace)信息中總是出現(xiàn)objc_msgSend。
要點(diǎn)
- 消息由接收者、選擇子及參數(shù)構(gòu)成彤灶。給某對象“發(fā)送消息”(invoke a message)也就相當(dāng)于在該對象上“調(diào)用方法”(call a method)看幼。
- 發(fā)給某對象的全部消息都要由“動態(tài)消息派發(fā)系統(tǒng)”(dynamic message dispatch system)來處理,該系統(tǒng)會查出對應(yīng)的方法幌陕,并執(zhí)行其代碼诵姜。
讀Effective Objective-C 2.0 有感