標籤:
介紹
Objective-C將許多決策從便宜時期和連結時期延後到運行時期。只要可能,它都動態做很多事情。這意味著它不僅需要一個編譯器,還需要一個運行時系統來執行編譯好的代碼。對於Objective-C來說,這個運行時系統就好像一個作業系統,使objective-c能夠正常工作。
本文探究NSObject類,以及Objective-C程式如何和運行時系統互動。
通過閱讀本文,你應該理解Objective-C的運行時系統如何工作,以及如何利用它。儘管對於寫一個Cocoa程式而言,你可能並不需要理解本文。
本文的組織
本文包括以下幾章:
- 運行時系統的版本和平台
- 和運行時系統互動
- 發送訊息
- 動態方法解析
- 訊息轉寄
- 類型編碼
- 聲明的屬性
參見
Objective-C運行時指南描述了Objective-C執行階段程式庫的資料結構和函數。你的程式可以使用這些介面和Objective-C運行時系統進行互動。例如,你可以對一個已經載入的類添加類和方法,或者擷取類定義列表。
Runtime的版本和平台
在不同的平台上有很多不同的Objective-C的Runtime的版本。
過去和現代的版本
Objective-C Runtime有2個版本——現代版本和過去版本。現代版本是Objective-C 2.0引入的,包括一系列的新特性。過去版本的編程介面在《Objective-C 1 Runtime Reference》中進行了描述,現代版本的編程介面在《Objective-C Runtime Reference》。
最值得注意的新特性是在現代runtime中執行個體變數是“non-fragile”的:
- 在過去版本中,如果你改變一個類中執行個體變數的布局,你必須重新編譯這個類的子類;
- 在現代版本中,如果你改變一個類的執行個體變數的布局,你不需要重新編譯這個類的子類;
另外,現代runtime支援 declared properties 的執行個體變數合成(詳情請參閱《The Objective-C Programming Language》)。
平台
iphone程式和運行在OSX v10.5及以上版本的64位的程式使用現代版的runtime,其它程式(OSX案頭的32位程式)使用過去版本的runtime。
和Runtime進行互動
Objective-C程式和Runtime的互動可以分為3個層次:通過Objective-C原始碼;通過Foundation framework 的 NSObject類的方法;通過直接調用Runtime函數。
Objective-C 原始碼
通常情況下,Runtime系統在螢幕背後自動運行,你僅僅通過寫和編譯Objective-C代碼來使用它。當你編譯帶有Objective-C的類和方法的代碼時,編譯器會建立實現語言動態特性的資料結構和函數調用。這些資料結構會捕獲類、category、protocol聲明中的資訊,包括我們在《Defining a Class and Protocols in The Objective-C Programming Language》中討論的類和protocol對象、selector、執行個體變數模板,以及原始碼中的其他內容。主要的Runtime函數是那些能發送訊息的函數,我們會在Messaging一節重點講解,它被原始碼的訊息運算式調用。
NSObject方法
Cocoa中大部分對象都是NSObject的子類,所以大部分對象繼承了它的方法。(值得注意的一個例外是NSProxy類,詳情見《訊息轉寄》這一節。)因此它的方法的行為也能夠被每一個對象和每一個類執行個體所繼承。然而,在一些情況下,NSObject類僅僅定義了一個模板,並沒有提供代碼。
例如說,NSObject類定義了一個description執行個體方法,這個方法返回描述類內容的字串。它主要用於調試——GDB的 print-object 會列印description方法返回的字串。NSObject並不知道這個類的內容,因此它只返回名字和對象的地址,NSObject的子類能夠返回更多細節。例如, NSArray的description方法返回它包括的對象的描述列表。
一些NSObject方法僅僅查詢Runtime的資訊。這些方法允許對象進行反省。一個例子是class方法,它 向對象詢問它的類,還有isKindOfClass:方法和isMemberOfClass:方法,它們檢測對象在類層次中的位置,respondsToSelector:檢測對象是否響應某個訊息,conformsToProtocol:檢測對象是否實現了特定協議的方法。這些方法給予了對象自省能力。
Runtime方法
Runtime系統是一個動態共用程式庫,它有公用介面,它的標頭檔位於 /usr/include/objc的檔案夾裡,有方法和資料結構的集合。當你寫Objective-C代碼時,這裡很多方法允許你使用C代碼重現編譯器做的事情。其它方法形成了功能基礎,這些功能通過NSObject類的方法匯出。寫Objective-C程式時,一些Runtime方法是很有用的。《Objective-C Runtime Reference》對這些所有的方法進行了介紹。
訊息
這一章將會講解訊息傳遞怎樣會轉化為objc_msgSend方法調用,以及你怎樣通過名字來引用方法,然後講解你怎樣利用objc_msgSend方法,以及在你需要的時候怎樣避免動態綁定。
objc_msgSend方法
在Objective-C中,直到運行時訊息才會被綁定到方法實現。編譯器會把一個訊息運算式
[receiver message]
轉化為一次對訊息函數——objc_msgSend的調用。objc_msgSend函數將訊息的接收者和訊息中的方法名作為參數。任何在訊息中傳遞的參數也同樣會被objc_msgSend處理:
objc_msgSend(receiver, selector, arg1, arg2, ...)
訊息函數為動態綁定做了一切:
- 它首先找到選取器引用的過程(方法實現)。由於相同的方法也可以被不同的類實現,因此它最終找到的過程依賴於接收者的類。
- 然後它會調用這個過程,傳給它接收的對象(指向它的資料的指標),以及這個方法需要的其它參數。
- 最後,它將過程的傳回值作為自己的傳回值。
注意:編譯器會產生對訊息函數的調用,你不應該在自己的代碼中直接調用它。
發送訊息的關鍵在於編譯器為每個類和對象產生的結構,每個類結構包括2個基本元素:
- 指向superclass的指標。
- 類的分發表。這個表中的條目是方法的選取器和特定類的方法地址的映射。例如setOrigin::的選取器就和setOrigin::的實現的地址進行了關聯,display的選取器和display的地址進行了關聯,等等。
當一個新對象建立以後,記憶體會被分配,它的執行個體變數會被初始化。這其中有一個指標指向它的類結構,這個指標就是 isa 指標,使得對象能夠訪問它的類,並且通過這個類它能夠訪問它的所有父類。
注意:嚴格來說,isa指標並不是語言本身的一部分,但它是對象和Objective-C Runtime系統協作所必需的。對象需要和objc_object結構各個欄位都相等。然而,你很少需要建立你自己的root對象,繼承自NSObject和NSProxy的對象自動有一個isa變數。
當一個訊息發送給一個對象時,訊息函數隨著isa指標在其類的訊息分發表中尋找方法選取器。如果找不到選取器,objc_msgSend就會跟隨指向父類的指標並在其方法分發表中嘗試尋找選取器,如果一直失敗objc_msgSend會沿著繼承體系不斷向上,直到到達NSObject類。一旦找到選取器,訊息函數會調用該方法並將接收對象的資料結構傳遞給它。
這就是運行時選擇方法實現的方式,也就是物件導向編程術語中方法被動態綁定到訊息的方式。
為了加速訊息的處理,Runtime系統會緩衝選取器和使用過的方法地址。每個類都有一個獨立的緩衝,它能儲存從父類繼承來的方法以及自己獨特的方法的選取器。在搜尋分發表之前,訊息路由會檢查接收訊息的對象的類的緩衝(理論上如果一個方法被使用過,那麼它可能會被再次使用)。如果方法選取器在緩衝中,那麼這個訊息處理僅僅比方法調用慢一點點。一旦一個程式運行很長時間去逐漸建立這個緩衝,那麼幾乎所有的訊息都會找到一個緩衝的方法。隨著程式的運行,緩衝會動態增長,以容納新的訊息。
使用隱藏參數
當objc_msgSend發現實現一個方法的過程時,它會調用這個過程,並通過訊息傳遞給它所有的參數,同時也會傳遞2個隱藏參數:
- 接收對象
- 方法選取器
這些參數會給方法實現關於和兩部分的明確的資訊,之所以說是隱藏,是因為他們沒有在方法的原始碼中明確的聲明。它們是在編譯的時候插入實現中的。
儘管這些參數沒有被明確聲明,原始碼仍然能夠引用他們(就好像它能夠引用接收對象執行個體變數一樣)。一個方法用self來引用接收對象,用_cmd來引用選取器。在下面的例子中,_cmd引用stange方法的選取器,self引用接收stange訊息的對象。
- 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 runtime系統提供的,不是Objective-C語言的特性。
動態方法解析
本章講解如何為一個方法動態提供實現。
動態方法解析
在某些場合你可能希望動態地給一個方法提供實現,例如說Objective-C語言的@dynamic關鍵字:
@dynamic propertyName;
它告訴編譯器這個方法將會被動態提供。
你能夠實現resolveInstanceMethod:和resolveClassMethod: 分別為一個執行個體對象和類動態地提供方法。
一個Objective-C方法是一個至少包括2個參數——self和_cmd的C函數。你可以利用函數class_addMethod將一個函數添加到類中,成為類的方法。對於如下函數:
void dynamicMethodIMP(id self, SEL _cmd) { // implementation ....}
你可以使用resolveInstanceMethod:把它添加類中成為類的方法,例如:
@implementation MyClass+ (BOOL)resolveInstanceMethod:(SEL)aSEL{ if (aSEL == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "[email protected]:"); return YES; } return [super resolveInstanceMethod:aSEL];}@end
轉寄方法(也叫訊息轉寄)和動態訊息解析很大程度上是強相關的。在轉寄機制啟動前,類有機會動態解析方法。如果使用respondsToSelector:或者instancesRespondToSelector: ,首先動態解析器可以為選取器提供一個IMP。如果你實現了 resolveInstanceMethod:但只想要特定的選取器被通過轉寄機制轉寄,你可以針對那些不想轉寄的選取器返回NO。
動態載入
Objective-C程式能夠在運行時載入和連結新的類和類別(catogery)。新代碼將和程式開始時載入的類和類別一樣很好的融入到程式中。
動態載入被用來做很多不同的事情。例如在系統偏好中的那些模組就是動態載入的。
在Cocoa環境中,動態載入通常被用來允許程式定製化。你的程式在運行時可以載入別人寫的一些模組——就好像Interface Builder載入定製的工具箱以及OSX系統偏好程式載入定製化偏好模組。
被載入的程式能夠擴充原來程式的功能。他們以一種你允許但又不能預期和定義它的行為的方式提供這些模組。你定義架構,他們實現代碼。
儘管在Mach-O檔案(objc_loadModules, 在objc/objc-load.h 中定義)中有方法執行Objective-C模組的動態載入,Cocoa的NSBundle類為動態綁定提供了一個更方便的介面——它是物件導向的並且和相關類整合。參閱Foundation framework reference的NSBundle類的說明以擷取NSBundle類及其用法。關於Mach-O的資訊請參閱《OS X ABI Mach-O File Format Reference 》。
訊息轉寄
向一個對象發送它不能處理的訊息會發生錯誤,然而,在聲明出錯之前,運行時系統給接收訊息的對象
一個機會來處理訊息。
轉寄
當你向一個對象發送它不能處理的訊息時,在它聲明出錯之前,runtime會給它發送forwardInvocation: 訊息,這個訊息有唯一的參數———一個NSInvocation對象,NSInvocation對象封裝了訊息本身以及訊息帶的參數。
你可以實現 forwardInvocation: 方法來給出一個該訊息的預設響應,或者通過其他方式避免錯誤的產生。就像它的字面意思一樣,forwardInvocation: 通常用來將訊息轉寄給其它對象。
關於轉寄的範圍和意圖,想象下面的情境:首先,假設你設計了一個對象,能夠響應 negotiate 訊息,你想要它的響應包括另外一個對象的響應。你可以很輕鬆的實現:你只需要在你實現的 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:需要做的是:
1. 決定訊息應該流向哪裡;
2. 將原始參數傳遞給它
這個訊息必須要能被invokeWithTarget: 發送:
- (void)forwardInvocation:(NSInvocation *)anInvocation{ if ([someOtherObject respondsToSelector: [anInvocation selector]]) [anInvocation invokeWithTarget:someOtherObject]; else [super forwardInvocation:anInvocation];}
被轉寄訊息的傳回值會返回給原始的寄件者。所有類型的傳回值都能夠被傳遞給寄件者,包括ids、結構和雙精確度浮點型數字。
forwardInvocation: 的作用就好像一個無法識別的訊息的分發中心,將訊息分發給不同的接收者。或者是一個轉化中心,發送所有的訊息到相同的目的地。它能將一個訊息翻譯成另外一個,也能夠“吞噬”訊息,沒有響應也沒有錯誤。forwardInvocation:也能將很多訊息合并成一個響應。forwardInvocation: 如何執行取決於實現者。它提供的這樣的機制使得在轉寄鏈中連結化物件成為了可能。
注意:只有當接收者沒有調用已有方法時forwardInvocation: 才會處理訊息。也就是說如果你想讓你的對象轉寄negotiate訊息給其他對象,那麼它就不能有一個negotiate方法。如果它有的話,訊息將永遠不能到達forwardInvocation:。
更多關於訊息轉寄和調用的資訊,請參與《 Foundation framework reference》中NSInvocation類的說明。
轉寄和多重繼承
轉寄類比繼承,轉寄能夠借用一些多重繼承的效果。一個對象通過轉寄響應了一個訊息,就好像從其它對象那裡借用或者繼承了一個方法實現一樣。
在中,一個Warrior對象轉寄negotiate訊息給一個Diplomat對象,Warrior對象就好像Diplomat對象一樣,看起來它能夠響應negotiate訊息了,在所有實際情況下它都能夠響應(儘管它是一個Diplomat對象)。
轉寄訊息的對象從而從繼承體系的兩個分支——它自己所在的分支和訊息響應者所在的分支“繼承”了這個方法。在上面的例子中,看起來好像Warrior對象同時繼承自Diplomat類和它自己的超類。
訊息轉寄提供了你想要從多重繼承獲得的大多數特性。然而兩者有一個顯著地不同:多重繼承將不同的功能組合在一個對象中,它側重於大、多層面的對象。而轉寄側重於將不同的能力分配給不同的對象。它將問題分解為小對象,以一種對訊息寄件者透明的方式將這些對象聯絡起來。
代理對象
轉寄不僅模仿多重繼承,而且也使得開發代表或覆蓋更多實體物件的輕量級對象成為可能。代理站在另外那個對象的角度,並且向它傳遞訊息。
代理使得向遠程對象轉寄訊息成為可能,參數可以正確的拷貝和擷取。但它不會拷貝遠程對象的功能,僅僅給遠程對象一個本地地址,通過這個本地地址它能夠接收訊息。
其他類別的代理對象也是可能的。例如你有一個對象,這個對象有大量資料——它可能建立了一個複雜的映像或者讀取了磁碟上檔案的內容。建立這個對象很耗時,因此你可能傾向於稍後建立——當需要它的時候或者系統資源閑置的時候。同時你需要這個對象的預留位置,以便程式中的其他對象可以正常工作。
在這種情況下,你可能開始不會建立一個完整的對象,而是一個輕量級的代理。這個對象能夠自己處理一些事情,例如回答關於資料的問題,但是它主要的功能還是作為這個大對象的預留位置,以及在合適的時候轉寄訊息。當這個代理對象的forwardInvocation: 方法接收到一個發給其他對象的訊息時,它會在該對象不存在是建立它以確保對象的存在。所有發給這個大對象的訊息都經過這個代理,對象和代理是等價的。
轉寄和繼承
儘管轉寄酷似繼承,但是NSObject類絕不會把兩者混淆起來。respondsToSelector: and isKindOfClass: 這兩個方法僅僅存在於繼承體系中,不會存在於轉寄鏈中。例如,一個Warrior對象是否響應 negotiate 方法的代碼為:
if ( [aWarrior respondsToSelector:@selector(negotiate)] ) ...
答案是否定的,儘管它能夠接收 negotiate 訊息並且做出反應,通過轉寄訊息給Diplomat對象。
在很多情況下,否定是正確的答案。但也可能不是這樣。如果你用轉寄去建立一個代理對象或者去擴充一個類,轉寄機制就和繼承機制一樣透明。如果你想要你的對象表現的好像它們真的繼承了你要轉寄到的對象的行為的話,你就需要重新實現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:已經包括了,請調用它。
注意:這是一個進階技術,僅適用於沒有其他解決方案的情況。他不應該作為繼承的替代品。如果你必須要使用該技術,請確認你已經完全理解了你要轉寄的類和轉寄到的類的行為。
本節提到的方法都在Foundation framework reference 的 NSObject 類的說明中有描述。關於invokeWithTarget: 的更多資訊,請參考 Foundation framework reference 中 NSInvocation 的說明。
類型編碼
(都是表格,暫不翻譯,原文地址:Objective-C Runtime Programming Guide)
屬性聲明
當編譯器遇到屬性聲明(參見《The Objective-C Programming Language》中的屬性聲明),它會為這個類、類別或者協議產生一些描述性的 metadata。你可以通過一些方法訪問這些metadata,這些方法能夠通過類或者協議的名字查詢屬性,擷取屬性的類型,以及拷貝屬性的屬性。屬性聲明的列表對每個類和協議都適用。
屬性類型和方法
Property 結構定義了屬性描述符的handle。
typedef struct objc_property *Property;
你可以使用 class_copyPropertyList 和 protocol_copyPropertyList 方法擷取一個類、類別或者協議的屬性列表:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
例如,給定如下類聲明:
@interface Lender : NSObject { float alone;}@property float alone;@end
你可以這樣擷取屬性列表:
id LenderClass = objc_getClass("Lender");unsigned int outCount;objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以使用 property_getName 來擷取屬性名稱:
const char *property_getName(objc_property_t property)
你可以用方法 class_getProperty 和 protocol_getProperty 擷取一個類或協議的指定名字的屬性的引用:
objc_property_t class_getProperty(Class cls, const char *name)objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用 property_getAttributes 方法擷取屬性的名字和 @encode 類型的字串。關於編碼類別型字串的細節,請參考 《類型編碼》章節,細節請參考 《屬性類型字串》和《屬性類型描述舉例》。
const char *property_getAttributes(objc_property_t property)
因此,你可以用下面這段代碼列印一個類的屬性列表:
id LenderClass = objc_getClass("Lender");unsigned int outCount, i;objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);for (i = 0; i < outCount; i++) { objc_property_t property = properties[i]; fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));}
屬性類型字串
你可以使用 property_getAttributes 方法來擷取屬性的名字、@encode類型字串,以及屬性的其他屬性。
字串以T開頭,然後是 @encode 類型和逗號,最後是V和執行個體變數的名字。在這之中,屬性由以下這些描述符指定:
表格7-1 屬性類型編碼
(表格不再翻譯,原文地址:Objective-C Runtime Programming Guide)
Objective-C Runtime Programming Guide 中文翻譯