引言
Objective-C語言從編譯時間和鏈接時間到運行時推遲了盡可能多的決策。只要有可能,它就會動態地執行任務。這意味著該語言不僅需要編譯器,還需要運行時系統來執行編譯的代碼。運行時系統充當Objective-C語言的一種操作系統,是語言工作的基礎。
與運行時交互
Objective-C程序在三個不同的級別與運行時系統交互:通過Objective-C源代碼,通過Foundation框架中的NSObject
類中定義的方法,通過直接調用運行時函數。
Objective-C源代碼
在大多數情況下,運行時系統會在后臺自動運行。我們只需要編寫和編譯Objective-C源代碼即可使用它。
當編譯包含Objective-C類和方法的代碼時,編譯器會創建實現該語言動態特性的數據結構和函數調用。數據結構捕獲在類和類別定義中以及在協議聲明中找到的信息,它們包括在The Objective-C Programming Language中Defining a Class和Protocols討論的類和協議對象,以及方法選擇器,實例變量模板和從源代碼中提取的其他信息。主要的運行時函數是發送消息的函數,如消息發送所述。它由源代碼消息表達式調用。
NSObject方法
Cocoa中的大多數對象都是NSObject
類的子類,因此大多數對象都繼承了它定義的方法。(值得注意的例外是NSProxy
類,有關更多信息請參看消息轉發。)因此,它的方法建立了每個實例和每個類對象固有的行為。然而,在少數情況下,NSObject
類只定義了應該如何完成某事的模板,它本身并不提供所有必要的代碼。
例如,NSObject
類定義了一個description
實例方法,該方法返回描述類內容的字符串。其主要用于調試——GDB的print-object
命令打印此方法返回的字符串。NSObject
的此方法的實現不知道該類包含什么,因此它返回一個包含對象名稱和地址的字符串。NSObject
的子類可以實現此方法以返回更多詳細信息。例如,Foundation框架中的NSArray
類的此方法返回它包含的對象的描述列表。
一些NSObject
方法只是查詢運行時系統的信息,這些方法允許對象執行內?。▋仁∈侵赣嬎銠C程序在運行時檢查對象類型的一種能力,通常也可以稱作運行時類型檢查)。這些方法的例子是class
方法,它請求對象標識它的類;isKindOfClass:
和isMemberOfClass:
方法檢驗對象在繼承層次結構中的位置;respondsToSelector:
方法指示對象是否可以接收特定消息;conformsToProtocol:
方法指示對象是否聲明實現特定協議中定義的方法;methodForSelector:
方法提供方法實現的地址。
運行時函數
運行時系統是一個具有公共接口的動態共享庫,其公共接口由位于/usr/include/objc
目錄中的頭文件中的一組函數和數據結構組成。其中許多函數允許我們使用純C語言來復制當我們編寫Objective-C代碼時編譯器所執行的操作,另一些函數構成了通過NSObject
類的方法輸出的功能的基礎。這些函數使得開發運行時系統的其它接口成為可能,并生成了增強開發環境的工具。在Objective-C中編程時不需要它們,但是,在編寫Objective-C程序時,一些運行時函數有時可能會有用。所有這些功能都記錄在Objective-C Runtime Reference中。
消息傳遞
本節介紹如何將消息表達式轉換為objc_msgSend
函數調用,以及如何按名稱引用方法。然后,還解釋了如何使用objc_msgSend
以及如何繞過動態綁定。
objc_msgSend函數
在Objective-C中,消息在運行時之前不會被綁定到方法實現,編譯器將消息表達式
[receiver message]
轉換為objc_msgSend
消息傳遞函數的調用。此函數將接收者和消息中提到的方法名稱(即方法選擇器)作為其兩個主要參數:
objc_msgSend(receiver, selector)
消息中傳遞的任何參數也會傳遞給objc_msgSend
:
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息傳遞函數會為動態綁定去做任何必要的事情:
- 它首先找到選擇器所引用的程序(即方法實現)。由于同樣的方法能夠被單獨的類以不同的方式實現,所以它找到的精確程序取決于接收者的類。
- 然后它調用該程序,將接收對象(指向其數據的指針)以及為該方法指定的任何參數傳遞給它。
- 最后,它將程序的返回值作為其自身的返回值傳遞。
消息傳遞的關鍵在于編譯器為每個類和對象構建的結構體。每個類結構都包括以下兩個基本要素:
- 一個指向父類的指針。
- 一個類調度表。此表含有將方法選擇器與這些方法選擇器所標識方法的特定于類的地址相關聯的條目。
setOrigin::
方法的選擇器與setOrigin::
(方法實現程序)的地址相關聯,display
方法的選擇器與display
的地址相關聯,依此類推。
當一個新對象被創建時,將為其分配內存,并初始化其實例變量。對象的第一個變量是指向其類結構的指針。這個名為isa
的指針使得對象能夠訪問它的類,并通過該類訪問它繼承的所有類。
類結構和對象結構的這些元素如下圖所示:
當一個消息被傳遞給對象時,objc_msgSend
函數跟隨對象的isa
指針到類結構的類調度表中查找方法選擇器。如果沒有找到方法選擇器,objc_msgSend
函數會跟隨指向父類的指針到父類的類調度表中查找方法選擇器。objc_msgSend
函數會順著類層次結構一直查找,直到到達NSObject
類。一旦找到方法選擇器,該函數就會調用類調度表中的方法并將接收對象的數據結構傳遞給它。
這是在運行時選擇方法實現的的方式——或者,在面向對象編程的術語中,方法動態地綁定到消息。
為了加快消息傳遞的速度,運行時系統會緩存使用過的方法的選擇器和地址。每個類都有一個獨立的緩存,它可以包含繼承的方法以及該類中定義的方法的選擇器。在檢索調度表之前,消息傳遞函數首先檢查接收對象類的緩存(理論上使用過一次的方法可能會再次使用)。如果緩存中存在方法選擇器,則消息傳遞僅比函數調用稍慢一點。一旦程序運行了足夠長的時間來“預熱”其緩存,它傳遞的幾乎所有消息都能找到一個緩存的方法。
使用隱藏參數
當objc_msgSend
找到實現一個方法的程序時,它會調用該程序并將消息中的所有參數傳遞給該程序。它還傳遞了兩個隱藏參數:
- 接收對象。
- 方法的選擇器。
這些參數為每個方法實現提供與調用該方法實現的消息表達式有關的明確信息,它們之所以被稱為隱藏參數是因為它們未在定義方法的源代碼中聲明。當代碼被編譯時,它們才會被插入到方法實現中。
雖然這些參數沒有顯示聲明,但源代碼仍然可以引用它們(就像它可以引用接收對象的實例變量一樣)。方法將接收對象引用為self
,并將其自身的選擇器稱為_cmd
。在下面的示例中,_cmd
引用為strange
方法的選擇器,self
引用為接收strange
消息的對象。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
self
是兩個參數中更加有用的一個。實際上,它是接收對象的實例變量可用于方法定義的一種方式。
獲取方法地址
繞過動態綁定的唯一方法是獲取方法的地址并直接調用它。這可能適用于極少數情況,例如,當一個特定方法將連續多次執行并且希望每次執行該方法時都避免消息傳遞的開銷時。
使用NSObject
類中定義的methodForSelector:
方法可以請求一個指向實現了一個方法的程序的指針,然后使用該指針調用方法實現程序。methodForSelector:
方法返回的指針必須小心地轉換為正確的函數類型。返回值和參數類型都應包含在強制轉換中。
以下示例顯示了實現了setFilled:
方法的程序是如何被調用的:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
傳遞給程序的前兩個參數是接收對象(self)和方法選擇器(_cmd)。這些參數隱藏在方法語法中,但在將方法作為函數調用時必須使其顯式化。
使用methodForSelector:
方法繞過動態綁定可以節省消息傳遞所需的大部分時間。但是,只有在特定消息重復多次的情況下,節省才會明顯,例如上面所示的for循環。
注意,methodForSelector
方法是由Cocoa運行時系統提供的,它不是Objective-C語言本身的一個特性。
動態方法解析
本節介紹如何動態提供方法的實現。
動態方法解析
在某些情況下,我們可能希望動態提供方法的實現。例如,Objective-C聲明屬性特性(參看Objective-C Programming Language中的Declared Properties)包含@dynamic
指令:
@dynamic propertyName;
它告訴編譯器將動態提供與屬性關聯的方法。
可以實現resolveInstanceMethod:
方法和resolveClassMethod:
方法來分別為實例方法和類方法的給定選擇器提供一個實現。
一個Objective-C方法在根本上是一個至少需要兩個參數(self和_cmd)的C函數,可以使用class_addMethod
函數將函數作為方法添加到類中。因此,給出以下函數:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
使用resolveInstanceMethod:
方法動態地將上面的函數作為方法(方法名為resolveThisMethodDynamically
)添加到一個類中:
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
轉發方法(如消息轉發中所述)和動態方法解析在很大程度上是正交的,類可以在轉發機制啟動之前動態解析方法。如果調用respondsToSelector:
方法或者instancesRespondToSelector:
方法,則動態方法解析器有機會首先為選擇器提供IMP
。如果實現了resolveInstanceMethod:
方法,但是希望特定的選擇器實際上是通過消息轉發機制轉發的,則為這些選擇器返回NO
。
動態加載
Objective-C程序可以在運行時加載和鏈接新類和類別。新代碼被合并到程序中,并且與最開始加載的類和類別進行相同的處理。
動態加載可以用來做很多不同的事情。例如, System Preferences應用程序(系統偏好設置)中的各種模塊就是動態加載的。
在Cocoa環境中,通常使用動態加載來允許自定義應用程序。其他人可以編寫程序在運行時加載的模塊——就像Interface Builder中加載自定義調色板和OS X System Preferences應用程序加載自定義偏好設置模塊一樣,可加載模塊擴展了應用程序的功能,他們以我們允許但是無法由我們自己預測和定義的方式為其做出貢獻。我們提供框架,但是其他人提供提供代碼。
雖然有一個運行時函數可以在Mach-O文件中執行Objective-C模塊的動態加載(objc_loadModules
,在objc/objc-load.h中定義),但是Cocoa的NSBundle
類為動態加載提供了一個非常方便的接口。有關NSBundle
類及其用法的信息,請參看NSBundle。
消息轉發
將消息發送給一個不能處理該消息的對象會引發錯誤。但是,在宣布錯誤之前,運行時系統給接收對象提供了第二次機會去處理該消息。
轉發
如果發送一個消息給不能處理該消息的對象,則在宣布錯誤之前,運行時系統會向該對象發送一個forwardInvocation:
消息,該消息唯一的參數是一個NSInvocation
對象——NSInvocation
對象封裝了原始的消息和該消息傳遞的參數。
可以實現forwardInvocation:
方法來為消息提供默認的響應,或者以其他方式來避免錯誤。顧名思義,forwardInvocation:
方法通常用于將消息轉發給另一個對象。
為了明白轉發的范圍和目的,請設想以下情景:首先,假設我們正在設計一個能夠響應名為negotiate
的消息的對象,并且希望其響應中包含另一種對象對該消息的響應??梢酝ㄟ^在negotiate
方法實現主體中的某個位置將negotiate
消息傳遞給另一個對象來輕松完成此操作。
更進一步,假設我們希望對象對negotiate
消息的響應完全是在另一個類中實現的響應。實現此目的的一種方法是讓類從其他類繼承該方法。然而,可能無法以這種方式安排事情,因為我們的類和實現了negotiate
方法的類可能位于繼承層次結構的不同分支中。
即使類不能繼承negotiate
方法,我們仍然可以通過實現一個簡單地將該消息傳遞給另一個類的實例的方法來借用它:
- (id)negotiate
{
if ( [someOtherObject respondsTo:@selector(negotiate)] )
return [someOtherObject negotiate];
return self;
}
這種方式可能會有點麻煩,特別是如果有許多需要對象傳遞給另一個對象的消息。我們必須實現一種方法來覆蓋每個想要從其他類借用的方法。而且,這樣可能無法處理那些我們不知道的情況。在編寫代碼時,我們可能無法確定想要轉發的完整消息集。該集合可能取決于運行時的事件,并且可能會在未來實現新的方法和類時發生改變。
forwardInvocation:
消息提供的第二次機會為這個問題提供了一個解決方案,其是動態的而不是靜態的。它的工作方式為:當一個對象無法響應消息是因為它沒有與該消息中的選擇器相匹配的方法時,運行時系統會通過向該對象發送一個forwardInvocation:
消息來告知它。每個對象都從NSObject
類繼承了一個forwardInvocation:
方法。但是,在NSObject
類的此方法實現中只是調用了doesNotRecognizeSelector:
方法。通過重寫該方法來覆蓋NSObject
類的實現,我們可以使用forwardInvocation:
消息提供的機會將消息轉發給其他對象。
要轉發一個消息,forwardInvocation:
方法需要做以下事情:
- 確定消息的去向。
- 使用原始的參數發送它。
可以使用invokeWithTarget:
方法發送消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
轉發的消息的返回值會返回給原始的消息接受者??梢詫⑺蓄愋偷姆祷刂祩鬟f給接收者,包括id
類型對象,結構體和雙精度浮點數。
forwardInvocation:
方法可以充當無法識別的消息的分發中心,將它們分發給不同的接收者?;蛘咚梢允寝D移站,將所有消息發送到同一個目的地。它可以將一條消息翻譯成另一條消息,或者只是“吞下”一些消息,這樣就沒有響應也沒有錯誤。forwardInvocation:
方法還可以將多個消息合并到一個響應中,該方法做什么事情取決于實現者。
注意:
forwardInvocation:
方法只有在對象調用一個其沒有實現的方法時才會處理消息。例如,如果希望對象將negotiate
消息轉發給另一個對象,則該對象不能擁有自己的negotiate
方法。否則,運行時系統永遠都不會發送forwardInvocation:
消息給該對象。
有關轉發和調用的更多信息,請參看NSInvocation。
轉發和多重繼承
轉發模仿了繼承,并可用于向Objective-C程序提供多重繼承的一些效果。如下圖所示,一個通過轉發消息來響應消息的對象好像借用或者“繼承”了另一個類中定義的方法實現。
轉發消息的對象因此從繼承層次結構的兩個分支“繼承”方法——它自己所在的分支和響應消息的對象所在的分支。在上面的例子中,似乎Warrior
類繼承自Diplomat
類以及它自己的父類。
轉發提供了我們通常想要從多重繼承中獲得的大多數特性,但是兩者之間有一個重要的區別。多重繼承在單個對象中組合了不同的能力,它傾向于大型、多方面的對象。另一方面,轉發是將單獨的責任分配給完全不同的對象,它將問題分解為較小的對象,但以對消息發送者透明的方式來關聯這些對象。
代理對象
轉發不僅模仿多重繼承,它還可以開發出代表或者“代替”有更多實質的對象的輕量級對象。代理代表另一個對象,并向該對象發送消息。
在The Objective-C Programming Language中Remote Messaging中討論的代理就是這樣的代理。代理處理將消息轉發到遠程接收者的管理細節,確保在連接過程中復制和檢索參數值,等等。但它并沒有嘗試做太多其他事情,它不會復制遠程對象的功能,而只是給遠程對象一個本地地址——一個可以在另一個應用程序中接收消息的地方。
其他類型的替代對象也是可能的。例如,假設有一個操縱大量數據的對象,也許它會創建一個復雜的圖像或者讀取磁盤上文件的內容。由于設置此對象可能會非常耗時,所以可以懶惰地執行此操作——在確實需要時或者系統資源暫時空閑時才執行此操作。同時,至少需要該對象的一個占位符才能使應用程序中的其他對象正常運行。
在這種情況下,可以初始創建一個不完整的對象,它只是一個輕量級的代理。這個對象可以自己做一些事情,比如回應和數據有關的問題,但大多數情況下,它只是為較大型的對象保留一個位置,并在時間到來時,轉發消息給它。當代理的forwardInvocation:
方法首次接收到發往另一個對象的消息時,它將確保該對象存在并且如果不存在則創建它。大型對象的所有消息都會通過代理,所以,就程序其余部分而言代理和大型對象是相同的。
轉發和繼承
雖然轉發模仿了繼承,但NSObject
類從來不會混淆兩者。像respondsToSelector:
和isKindOfClass:
這樣的方法只查看繼承層次結構,永遠不會查看轉發鏈。例如,如果詢問Warrior
對象是否響應negotiate
消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
返回值是NO
,盡管它可以毫無錯誤地接收negotiate
消息,并且從某種意義上來說,通過轉發該消息給Diplomat
對象來回應該消息。(請看圖4-1 轉發.png)
在許多情況下,NO
是正確答案,但某些情況下可能不是。如果使用轉發來設置一個代理對象或者擴展類的功能,則轉發機制可能應該像繼承一樣透明。如果希望對象的行為就像它們真正繼承了它們轉發消息的對象的行為一樣,那么需要重新實現respondsToSelector:
方法和isKindOfClass:
方法來包含我們的轉發算法:
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
除了respondsToSelector:
和isKindOfClass:
方法之外,instancesRespondToSelector:
方法還應該反映轉發算法。如果使用了協議,則同意應將conformsToProtocol:
方法加入到列表中。類似的,如果一個對象轉發它收到的任何遠程消息,它應該重新實現methodSignatureForSelector:
方法來返回最終響應轉發消息的方法的準確描述。例如,如果一個對象能夠將消息轉發給它的代理,那么實現methodSignatureForSelector:
方法如下:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
可以考慮將轉發算法放在私有代碼中的某個位置,并使用所有這些方法(包括forwardInvocation:
)來調用它。
注意:這是一項高級技術,僅適用于無法提供其他解決方案的情況。它不是作為繼承的替代。如果必須使用此技術,請確保完全了解執行轉發的類和轉發對象類的行為。
完整的消息轉發機制流程
向對象發送一個其不能處理的消息后,如果動態方法解析未成功,則會啟動消息轉發機制。
首先,運行時系統調用對象的forwardingTargetForSelector:
方法詢問是否存在該消息的后備接收者。則將消息發送給這個后備接收者,消息轉發完成。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"%@ --> forwardingTargetForSelector",[self class]);
if (aSelector == @selector(playMusic))
{
return [[AudioPlayer alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
如果不存在,運行時系統會調用methodSignatureForSelector:
方法獲取該方法的簽名并將其封裝成一個NSInvocation
對象,然后調用forwardInvocation:
方法并將NSInvocation
對象傳遞給它。在forwardInvocation:
方法實現中將這個消息發送給合適的對象或者廢棄這條消息,消息轉發機制完成。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%@ --> methodSignatureForSelector",[self class]);
if (aSelector == @selector(pause))
{
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return methodSignature;
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"%@ --> forwardInvocation",[self class]);
if (anInvocation.selector == @selector(pause))
{
[anInvocation invokeWithTarget:[[AudioPlayer alloc] init]];
}else
{
[super forwardInvocation:anInvocation];
}
}
類型編碼
為了協助運行時系統,編譯器會將每個方法的返回值和參數的類型編碼為字符串,并將此字符串與方法選擇器相關聯。它使用的編碼方案在其他上下文中也很有用,所以可以公開使用@encode()
編譯器指令。當給定一個類型規范時,@encode()
返回編碼該類型的字符串。類型可以是基本類型,例如int
,指針,標記結構或聯合,或者類名——實際上,任何類型都可以用作C運算符sizeof()
的參數。
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);
下表列出了類型代碼。注意,它們中的許多與編碼對象時用于存檔和分發的代碼重疊。但是,此處列出的代碼在編寫編碼器時是無法使用的,并且在編寫不是由@encode()
生成的編碼器時可能需要使用代碼。
Code | Meaning |
---|---|
c | A Char
|
i | An int
|
s | A short
|
l | A long l is treated as a 32-bit quantity on 64-bit programs. |
q | A long long
|
C | An unsigned char
|
I | An unsigned int
|
S | An unsigned short
|
L | An unsigned long
|
Q | An unsigned long long
|
f | An float
|
d | A double
|
B | A C++ bool or a C99 _Bool
|
v | A void
|
* | A character string (char * ) |
@ | An object (whether statically typed or typed id ) |
# | A class object (Class ) |
: | A method selector (SEL ) |
[array type] | An array |
{name=type...} | A structure |
(name=type...) | A union |
bnum | A bit field of num bits |
^type | A pointer to type |
^type | A pointer to type |
? | An unknown type (among other things, this code is used for function pointers |
重要:Objective-C不支持
long double
類型。@encode(long double)
返回d
,這與double
的編碼相同。
數組的類型代碼使用方括號括起來,數組中元素的數量是在數組類型前面的開括號后面立即指定的。例如,一個包含12個float
指針的數字將被編碼為:
[12^f]
結構體在括號內指定。首先列出結構體標簽,然后是等號,并按順序列出結構體字段的代碼。例如,結構體
typedef struct example {
id anObject;
char *aString;
int anInt;
} Example;
會被編碼成這樣:
{example=@*i}
結構體指針的編碼與結構體的字段有關的相同數量的信息:
^{example=@*i}
但是,指向結構體指針的指針的編碼間接刪除內部類型規范:
^^{example}
對象被視為結構體。例如,將NSObject
類名傳遞給@encode()
會產生以下編碼:
{NSObject=#}
NSObject
類只聲明一個類型為Class
的實例變量isa
。
注意,盡管@encode()
指令不返回下表中列出的其他編碼,但是當它們用于在協議中聲明方法時,運行時系統使用它們來表示類型限定符。
Code | Meaning |
---|---|
r | const |
n | in |
N | inout |
o | out |
O | bycopy |
R | byref |
V | oneway |