iOS 運行時
Objective-C語言進可能將許多決策從編譯和鏈接延緩到運行時。它盡可能的動態處理事務。這意味著Objective-C不僅需要編譯器還需要執行編譯代碼的運行時系統。運行時系統充當Objective-C的一種操作系統,使之正常工作。
Objective-C 程序與運行時系統的交互主要在三個不同的層次
通過Objective-C 源代碼;通過基礎框架中NSObject類中定義的方法;通過直接調用運行時函數。
- Objective-C 源代碼
在大多數情況下,運行時系統自動在后臺工作。只通過編寫和編譯Objective-C源代碼才會使用到。
當編譯包含Objective-C類和方法的代碼,編譯器會創建數據結構和函數調用,實現語言的動態特性。數據結構捕獲類和分類定義以及協議中聲明的信息,其中包括Objective-C編程語言中定義類和協議中討論的類和協議,也包括方法選擇器、實例變量模板和從源碼中提取的其他信息。運行時函數的主要功能是發送消息。被源碼消息表達式調用。 - NSObject 方法
Cocoa中大多數對象都是NSObject的之類,所以大多數對象都繼承NSObject定義的方法。(NSProxy類是個例外,更多信息參見消息轉發。)其方法因此為每個實例每個類對象建立行為。然而,在一些情況下,NSObject類只定義了一個模板,告知如何完成,并沒有提供必要的代碼。
例如,NSObject類定義了一個描述實例方法,該方法返回一個字符串描述類的內容。這主要用于調試,GDB打印對象命令打印該方法返回的字符串。NSObject方法的實現不知道類包含的內容,所以它返回的是一個字符串對象的名稱和地址。NSObject的之類可以實現這個方法并返回更多詳情。例如,基礎類NSArray返回一個列表,包含對象的表述。
NSObject的一些方法可以簡單的查詢運行時系統信息。這些方法運行對著執行自省。例子中的這種方法是類放,訪問一個對象來確定它的類;isKindOfClass:和isMemberOfClass:測試對象在繼承層次結構中的位置;respondsToSelector:表明一個對象是否可以接收特定消息;conformsToProtocol:表明一個對象是否要求實現特定協議中定義的方法;methodForSelector:提供方法實現的地址。類似這樣的方法給對象自我反省的能力。 - 運行時函數
運行時系統是一個動態共享庫,包含公共接口組成的一組函數和數據結構,其頭文件位于目錄/usr/include/objc。這些函數允許使用純C復制當編寫Objective-C代碼時編譯器生成的代碼。通過NSObject類方法導出其他形式的基礎功能。這些函數可以開發運行時系統的其他接口和產生可以擴大開發環境的工具,Objective-C編程中不需要他們。然而,一些運行時函數有時候在編寫Objective-C程序時很有用。所有的這些函數在Objective-C編程引用中有說明。
消息傳遞
將消息表達式轉換成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,通過類,對象可以訪問該類和該對象繼承的所有類。
注意:isa指針雖然不是語言嚴格意義上的一部分,但是是使用Objective-C運行時系統所需的一個對象。一個對象須“等效于”結構定義中的struct objc_object(定義于objc/objc.h)。然而,很少需要創建自己的根對象和繼承自NSObject 或NSProxy 的對象,自動有isa變量。
當一個消息發送到一個對象,消息傳遞函數遵循對象的isa指針,該指針指向類結構,并在dispatch表中查找方法選擇器。如果不能找到選擇器,objc_msgSend則遵循指向父類的指針并試圖在dispatch表找到選擇器。一直找不到選擇器,objc_msgSend將一直查找類的層次結構,直到NSObject類。一旦定位到選擇器,函數將調用表中的方法,并將其傳遞到接收對象的數據結構。
運行時選擇以這種方式實現方法。或者以面向對象編程術語來說,該方法是動態綁定到消息。
為了加快消息傳遞過程,運行時系統緩存使用的方法的選擇器和地址。每個類有一個單獨的緩存,可以包含繼承方法和類中定義方法的選擇器。在搜索dispatch表之前,消息傳遞程序首先檢查接收對象類(理論上,是有可能再次使用的方法)的緩存。如果方法選擇器在緩存中,消息傳遞稍微比函數調用慢。一旦一個程序運行足夠長時間來“熱身”緩存,幾乎所有發送的消息都能找到緩存方法。在程序運行時,緩存能動態適應新消息。
使用隱式參數
當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循環,節省時間才會有重要意義。
注意:Cocoa運行時系統提供methodForSelector:方法,該方法并不是Objective-C 語言本身的特性。
動態方法解析
如何動態的提供一個方法的實現。
在有些情況下,需要動態的提供一個方法的實現。例如,Objective-C 聲明的屬性特征(見Objective-C 編程語言中的聲明屬性)包含@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
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
"v@:" v的意思是參數 void 后邊固定@:
參見文檔
動態加載
一個 Objective-C 程序可以在運行過程中加載和鏈接新類和分類。程序中納入新代碼,在開始加載的類和類別中都可以使用。
動態加載可以用來做很多不同的事情。例如,系統偏好設置應用程序中的模塊都是動態加載的。
在Cocoa 環境中,動態加載通常用來自定義應用程序。其他人可以編寫模塊讓你的程序在運行時加載,類似IB加載自定義調色板和OS X系統設置應用程序加載自定義偏好模塊。可加載模塊可擴展應用程序。他們以你允許的方式貢獻代碼,但是不能自己預計和定義。你提供框架,其他人提供代碼。
盡管有一個運行時函數在Mach-O 文件(在objc/objc-load.h中定義的objc_loadModules)中,執行 Objective-C 模塊的動態加載。Cocoa的NSBundle 類為動態加載提供了更方便的接口,這個接口是面向對象并與相關服務結合。了解NSBundle 類信息和使用,可參閱基礎框架引用中的NSBundle 類規范。Mach-O 文件信息可查看OS X ABI Mach-O文件格式引用。
消息轉發
發送消息到不處理該消息的對象會發生錯誤。然而,在聲明錯誤之前,運行時系統給接收對象第二次機會處理該消息。
可以借此實現多重繼承的功能
如果發送消息到不處理該消息的對象,在聲明錯誤之前,運行時給該對象發送forwardInvocation: 消息,NSInvocation 對象作為唯一參數。NSInvocation 對象封裝原始消息和需要傳遞的參數。
可以實現 forwardInvocation:方法,提供一個默認消息響應,或者以其他方式避免錯誤。顧名思義, forwardInvocation:通常用來將消息轉發給另一個對象
為了看到轉發的范圍和目的,想象以下場景:首先假設,你正在設計一個叫做negotiate的對象可以響應消息,你希望它能響應另一個對象的響應。你可以通過傳遞negotiate消息到你實現的negotiate方法中的另一個對象。
更近一步,假設希望對象精確的響應negotiate 消息,則需要在另一個類中實現。實現這個目標的一個方法是讓類繼承其他類的方法。然而,它不可能以這種方式安排事情。也許存在充分的理由為什么你的類和實現negotiate 的類在不同分支的繼承層次結構中。
即使類不能繼承negotiate 方法,仍然可以通過實現一個版本的方法來“借用”它,該方法只是簡單的將信息傳遞給另一個類的實例:
-(id)negotiate
{
if([someOtherObject respondsTo:@selector(negotiate)])
return [someOtherObject negotiate];
return self;
}
這種方式有點麻煩,特別是對象要傳遞大量消息到另一個對象。你必須實現一個方法來覆蓋每個從其他類借來的方法。此外,這種方法不能處理你不知道的情況。例如,在寫代碼的時候,你想轉發所有的消息。這取決于運行時的事件,有可能在將來作為新方法和類實現。
消息提供第二次機會,提供了一個相對不那么特殊的解決方案,該方案是動態的而非靜態的。它的工作原理是:當一個對象因為沒有一個方法匹配消息中的選擇器而無法響應消息時,運行時系統通過發送一個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];
}
轉發消息的返回值返回給原始發送者。所有類型的返回值都可以傳遞給發送者,包括id,結構,雙精度浮點數。
forwardInvocation: 方法可以作為無法識別消息的分發中心,將消息打包給不同的接收者。或者可以作為中轉站,發送所有消息到相同的目的地。它可以把一個消息轉發給另一個,或簡單的“吞咽”一些消息,所有沒有響應也沒有錯誤。forwardInvocation:方法還可以合并幾個消息到一個響應上。forwardInvocation:做什么是由系統決定的。然而,它提供一個機會,使得轉發鏈接中的鏈接對象為程序設計提供可能。
注意:方法只有在他們不調用名義上接收者的現有方法的情況下才處理消息。例如,如果你希望你的對象轉發negotiate 消息到另一個對象,則對象不能有自己的negotiate 方法。如果是這樣,消息永遠不會到forwardInvocation:。
關于轉發和調用的更多信息,參見基礎框架引用中NSInvocation 類規范。
轉發和多重繼承
轉發提供了大部分多重繼承的功能。然而,兩者之間有個重要的區別:多重繼承在單一對象上結合了不同的功能。它更加強大,多層面對象。另一方面,轉發分配單獨的職責給不同的對象。它將問題分解為更小的對象,但是以一種方式將對象聯合,該方式對消息發送者是透明的。
代理對象
轉發不僅模仿多重繼承,還可以開發輕量級對象代表或“覆蓋”更實質的對象。代理代替其他對象并傳送消息給它。
Objective-C 編程語言中“遠程消息傳遞”中描述了該代理。代理負責管理消息轉發到遠程接受者,確保復制參數值和恢復鏈接,等等。但不嘗試做其他。它不與遠程對象的功能重復,只是簡單的給遠程對象一個本地地址,該地址可以在另一個應用程序中接收消息。
其他類型的代理對象也是可以的。假設,如果有個對象操作大量數據,它也許會創建一個復雜的圖片或者讀取磁盤上的文件內容。設置該對象可能非常耗時,所以為了簡單,只有真正需要的時候或者系統資源暫時閑置時使用。同時,為了保證其他對象在應用程序正常運行,該對象至少需要一個占位符。
在這樣的情況下,你可以首先創建,不是成熟的對象,但是時一個輕量級的代理。這個對象可以做些自己的事情,比如回答數據問題,但更多的是為大對象占個位置,當時間到了轉發消息給大對象。當代理forwardInvocation: 方法首先接收一條消息傳遞給另一個對象,它將確保對象存在,如果不存在則創建。所有大對象的消息都是通過代理,因此,對于其余程序而言,代理和大對象是一樣的。
轉發個繼承
盡管轉發模仿繼承,NSObject類不會混淆兩者。像respondsToSelector: 方法和isKindOfClass: 方法只查看繼承層次結構,不在轉發鏈上。
如果使用轉發來設置代理對象或擴展一個類的功能,轉發機制必須如同繼承一樣透明。如果想讓你的對象假裝它們真正繼承它們轉發消息的對象的行為,需要重新實現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;
}
注意:這是一種先進技術,只有在沒有其他解決方案可行的情況下才適用。它并不打算取代繼承。如果你必須使用這種技術,確保你完全理解轉發類和你要轉發的類的行為。
類型編碼
為了協助運行時系統,編譯器用字符串為每個方法的返回值和參數類型和方法選擇器編碼。使用的編碼方案在其他情況下也很有用,所以它是public 的,可用于@encode() 編譯器指令。當給定一個類型參數,返回一個編碼類型字符串。類型可以是一個基本類型如int,指針,結構或聯合標記,或任何類型的類名,事實上,都可以作為C sizeof() 運算符的參數。
--
聲明屬性
當編譯器遇到屬性聲明時(參見The Objective-C 編程語言中的聲明屬性),它生成與封閉類、分類或協議相關的描述性元數據??梢酝ㄟ^函數訪問元數據,該函數支持通過類或協議名稱查找屬性,獲取屬性的類型作為@encode 字符串,并復制property的屬性列表作為C字符串數組。聲明的屬性列表可用于每個類和協議。
屬性類型和函數
屬性結構為屬性描述符定義了一個不透明句柄。
typedef struct objc_property *Property;
可以使用class_copyPropertyList 和 protocol_copyPropertyList 函數分別檢索屬性數組和類(包括加載的類別)和協議:
objc_property_t *class_copyPropertyList(Class cls, unsigned intint *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned intint *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 charchar *property_getName(objc_property_t property)
可以使用class_getProperty和protocol_getProperty函數分別獲取類和協議中給定名稱的屬性的引用:
objc_property_t class_getProperty(Class cls, const charchar *name)
objc_property_t protocol_getProperty(Protocol *proto, const charchar *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
可以使用property_getAttributes 函數獲取屬性的名稱和@encode類型字符串。編碼類型字符串詳情可查看類型編碼;字符串詳情可查看屬性類型字符串和property屬性描述例子。
const charchar *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));
}