簡介
前面的文章了解了OC對象(Objective-C對象解析),本文將簡單介紹Objective-C消息傳遞的消息傳遞機制。
Objective-C 是 C的超集,C語言的函數調用方式,使用“靜態綁定”(static binding),在編譯期就能決定運行時所應調用的函數。而“動態綁定”(dynamic binding),所要調用的函數直到運行期才能確定,帶調用的函數地址無法硬編碼在指令之中,而是要在運行期讀取出來。
Objective-C中如果向某對象傳遞消息,就會使用動態綁定機制來決定需要調用的方法。在底層,所有方法都是普通的C語言函數,然而對象收到消息之后,究竟該調用哪個方法則完全于運行期決定,甚至可以在程序運行時改變,這些特性使OC為一門真正的動態語言。
Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個運行時系統來動態得創建類和對象、進行消息傳遞和轉發。
理解objc_mesgSend的作用
類型為id類型的對象,編譯器假定它能相應所有消息。編譯器無法確定某類型對象能解讀多少種選擇器,因為運行期還可向其中動態新增。如果聲明指定了具體類型,那么在該類實例上調用其所沒有的方法時,編譯器會探知此情況,并發出警告信息。
在運行期檢視對象類型這一操作也稱為 類型信息查詢(introspection, 內省)
id returnValue = [someObject messageName:parameter];
someObject: “接收者”receiver
messageName: “選擇器”selector,選擇器與參數合起來稱作“消息”message
編譯器看到此消息后,將其轉換為一條標準的C語言函數調用,所調用的函數是消息傳遞機制中的核心函數, void objc_msgSend(id self, SEL cmd, …) 這是個參數可變的函數 (variadic function),能接收兩個或兩個以上的參數。第一個參數代表接收者,第二個參數代表選擇器(SEL 是選擇器的類型)選擇器指的就是方法的名字,后續參數就是消息中的那些參數,其順序不變。編譯器會把上面的例子中的消息轉換為如下函數:
id returnValue = objc_msgSend(someObject, @selector(messageName:),parameter);
objc_msgSend函數會根據接收者與選擇器的類型來調用適當的方法。為了完成此操作,該方法需要在接收者所屬的類中搜尋其方法列表,如果能找到與選擇器名稱相符的方法,就跳至其實現代碼。若是找不到,就沿著繼承體系繼續向上查找,等找到合適的方法之后再跳轉。如果最終還是找不到相符的方法,就執行消息轉發機制。
這樣來看調用一個方法似乎需要很多步驟。所幸objc_msgSend會將匹配結果緩存在“快速映射表”里面,每個類都有這樣一塊緩存,若是后面還向該類發送與選擇器相同的消息,那么執行起來就很快。當然這種“快速執行路徑”還是不如“靜態綁定的函數調用操作”那樣迅速,不過只要把選擇器緩存起來,也不會慢很多。實際上消息派發并非應用程序的瓶頸所在,假如真是個瓶頸的話,可以只編寫純C函數,在調用時根據需要,把OC對象的狀態傳進去。
之前只是描述了部分消息的調用過程,其他特殊情況則需要交由OC運行環境中的另一些函數來處理:
objc_msgSend_stret: 待發送的消息要返回結構體,那么可交由此函數處理。只有當CPU的寄存器能夠容納得下消息返回類型時,這個函數才能處理此消息。若是返回的結構體過大,那么就由另一個函數執行派發,會通過分配在棧上的某個變量來處理消息所返回的結構體。
objc_msgSend_fpret: 消息返回的是浮點數,那么可交由此函數處理。這個函數是為了處理x86 等架構CPU中某些奇怪的狀況。
objc_msgSengSuper: 如果要給超類發消息,那么就交給此函數處理。也有另外兩個與objc_msgSend_stret objc_msgSend_fpret等效的函數,用于處理發給super的相應消息。
objc_msgSend等函數一旦找到應該調用的方法實現之后,就會跳轉過去,之所以能這樣做,是因為OC對象的每個方法都可以視為簡單的C函數,其原型類似于:
<return_type> Class_selector(id self, SEL _cmd, …)
每個類里都有一張表格,其中的指針都會指向這種函數,而選擇器的名稱則是查表時所用的“鍵”。objc_msgSend等函數正是通過這張表格來尋找應該執行的方法并跳至其實現的。
注意,原型的樣子和objc_msgSend等函數很像,這是為了利用“尾調用優化”(tail-call optimization)技術,令“跳轉方法實現”這一操作變得更簡單些。
如果某函數的最后一項操作是調用另外一個函數,那么就可以運用尾調用優化技術。編譯器會生成調轉至另一函數所需的指令碼,而且不會向調用堆棧中推入新的棧幀。只有當某些函數的最后一個操作僅僅是調用其他函數而不會將其返回值另做他用時,才能執行尾調用優化。這項優化對objc_msgSend非常關鍵,如果不這么做,那么每次調用OC方法之前都需要為調用objc_msgSend函數準備“棧幀”,在棧蹤跡(stack trace)中可以看到這種棧幀。此外,若是不優化,還會過早地發生“棧溢出”(stack overflow)現象。
小結:
1.消息有接收者、選擇器及參數,構成。給某對象發消息,相當于在該對象上調用方法
2.發給某對象的全部消息都要由動態消息派發系統(dynamic message dispatch system)來處理,該系統會查出對應的方法,并執行其代碼。
理解消息轉發機制
對象在收到無法解讀的消息之后會發生什么情況 ?
在編譯期向類發送了其無法解讀的消息并不會報錯,因為在運行期可以繼續向類中添加方法,所以編譯器在編譯時還無法確知類中到底會不會有某個方法實現。當對象收到無法解讀的消息后,就會啟用“消息轉發”(message forwarding)機制,程序員可以經此過程告訴對象應該如何處理未知消息。
消息轉發分為兩大階段:
第一階段先征詢接收者,所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇器”,這叫做“動態方法解析”(dynamic method resolution).
第二階段涉及完整的消息轉發機制,如運行期系統已經把第一階段執行完了,那么接收者自己就無法再以動態新增方法的手段來響應包含該選擇器的消息了。
運行期系統會請求接收者以其他手段來處理與消息相關的方法調用,可細分為兩步:
首先,請接收者看看有沒有其他對象能處理這條消息,若有,則運行期系統會把消息轉給那個對象,于是消息轉發結束。
若沒有“備援的接收者”(replacement receiver),則啟動完整的消息轉發機制,運行期系統會把與消息有關的全部細節都封裝到NSInvocation對象中,再給接收者最后一次機會,令其設法解決當前還未處理的這條消息。
動態方法解析
對象在收到無法解讀的消息后,首先將調用其所屬類的下列方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector;
該方法的參數就是那個未知的選擇器,其返回值表示這個類是否能新增一個實例方法用以處理此選擇器。再繼續往下執行轉發機制之前,本類有機會新增一個處理此選擇器的方法。假如尚未實現的方法不是實例方法,而是類方法,那么運行期系統會調用另外一個方法,“resolveClassMethod:”。
使用這種辦法的前提是:相關方法的實現代碼已經寫好,只等著運行時候動態插在類里面就可以了。此方案常用來實現@dynamic屬性。
備援接收者
當前接收者還有第二次機會能處理未知的選擇子。這一步,運行期系統會問當前接收者:能不能把這條消息轉給其他接收者來處理。與該步驟對應的處理方法:
- (id)forwardingTargetForSelector:(SEL)selector
若當前接收者能找到備援對象,則將其返回,若找不到返回nil。通過此方案,可以用“組合”來模擬出多重繼承的某些特性。在一個對象內部,可能還有一些列其他對象,該對象可經由此方法將能夠處理某選擇器的相關內部對象返回,這樣的話,在外界看來,好像是該對象親自處理了這些消息。
注意,我們無法操作經由這一步所轉發的消息。若是想在發送給備援接收者之前先修改消息內容,那就得通過完整的消息轉發機制來做了。
完整的消息轉發
到了這一步,唯一能做的就是啟用完整的消息轉發機制。首先創建NSInvocation對象,把與尚未處理的那條消息有關的全部細節都封裝于其中。此對象包含選擇器、目標及參數。在觸發NSInvocation對象時,消息派發系統將出馬,把消息指派給目標對象,此步驟會調用下列方法來轉發消息:
- (void)forwardInvocation:(NSInvocation *)invocation
這個方法可以實現的很簡單:只需改變調用目標,使消息在新目標上得以調用即可。然而這樣實現出來的方法與備援接收者方案所實現的方法等效,所以很少有人采用這么簡單的實現方式。比較有用的實現方式為:在觸發消息前,先以某種方式改變消息內容,如增加另外一個參數,或是改換選擇器等。
實現此方法時,若發現某調用操作不應該由本類處理,則需要調用超類的同名方法。這樣繼承體系中的每個類都有機會處理此調用請求,直至NSObject。如最后調用了NSObject類的方法,那么該方法還會繼而調用“doesNotRecognizeSelector:”以拋出異常,此異常表明選擇器最終未能得到處理。
接收者在每一步中均有機會處理消息,步驟越往后,處理消息的代價越大。最好能在第一步就處理完,這樣運行期系統就可以將此方法緩存起來。若這個類的實例后面還收到同名選擇器,那么就根本無須啟動消息轉發流程。
小結:
1.若對象無法響應某個選擇器,則進入消息轉發流程
2.通過運行期的動態方法解析功能,可以在需要用到某個方法時在將其加入類中
3.對象可以把其無法解讀的某些選擇器轉交給其他對象來處理
4.經過上兩步驟,若還是沒辦法處理選擇器,那就啟動完整的消息轉發機制。
用“方法調配技術”調試“黑盒方法”
OC對象收到消息后,要在運行期才能解析出來究竟會調用何種方法。給定的選擇器名稱相對應的方法也可以在運行期改變。這樣我們不需要源代碼,也不需要通過繼承子類來覆寫方法就能改變這個類本身的功能。這樣新功能將在本類的所有實例中生效,而不是僅限于覆寫了相關方法的那些子類實例。此方案經常稱為“方法調配”。
類的方法列表會把選擇器的名稱映射到相關的方法實現上,使得“動態消息派發系統”能夠據此找到應該調用的方法。這些方法均以函數指針的形式來表示,這種指針叫做IMP:id (*IMP)(id, SEL, ...)
NSString可以相應 lowercaseString..等,映射表的每個選擇器都映射到不同IMP上: lowercaseString —> IMP ..
OC的運行期系統提供的幾個方法都能用來操作這張表。開發者可以向其中新增選擇器,也可以改變某選擇器所對應的方法實現,還可以交換兩個選擇器所映射到的指針。
互換兩個方法實現:
#import <objc/runtime.h>
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
NSString * string = @"Hello! Nice to meet you.";
NSString * lowercaseString = [string lowercaseString];
//HELLO! NICE TO MEET YOU.
NSString * uppercaseString = [string uppercaseString];
//hello! nice to meet you.
可以通過這一手段來為既有的方法實現增添新功能,如想要在lowercaseString調用時記錄某些信息,這時可以通過交換方法來達到此目標。
可為那些完全不知具體實現的黑盒方法增加日志功能,有助于程序調試。此做法只在調試程序時有用,很少會在調試程序之外來永久改動某個類的功能。若是濫用會讓代碼不易讀難以維護。
References
http://draveness.me/method-struct.html
http://tech.glowing.com/cn/objective-c-runtime/
http://www.lxweimin.com/p/6b905584f536
《Effective Objective-C 2.0》