標籤:
Objective-C 2.0 的運行時環境叫做Morden Runtime,iOS 和Mac OS X 64-bit 的程式都運行在
這個環境,也就是說Mac OS X 32-bit 的程式運行在舊的Objective-C 1.0 的運行時環境Legacy
Runtime,這裡我們只講解Morden Runtime。
同運行時互動主要在三個不同的地方,分別是A.Objective-C 源碼(譬如:你定義的Category
中的新方法會在運行時自動添加到原始類)、B.NSObject 的方法(isMemberClassOf 等動態判
定的方法)、C.運行時函數。由於前兩者在第一篇文檔中講解過,這裡我們講一下運行時函
數的相關內容。
(1.)isa指標:
NSObject 中有一個Class isa 的指標類型的成員變數,因為我們的對象大都直接或者間接的從
NSObject 繼承而來,因此都會繼承這個isa 成員變數,isa 在運行時會指向對象的Class 對象,
一個類的所有對象的Class 對象都是同一個(JAVA 也是如此),這保證了在記憶體中每一個類
型都有唯一的類型描述。這個Class 對象中也有個isa 指標,它指向了上一級的父類的Class
對象。
在明白了這個isa 之後,你就可以明白在繼承的時候,A extends B,你調用A 的方法a(),首
先A 的isa 到A 的Class 對象中去尋找a()方法,找到了就調用,如果沒找到,就驅使A 的Class
對象中的isa 到父類B 的Class 對象中去尋找。
(2.)SEL 與IMP:
第一篇文檔中,我們提到了方法選取器SEL,它可以通過如下兩種方式獲得:
(SEL) @selector(方法的名字)
(SEL) NSSelectorFromString(方法的名字的字串)
另外,你還可以通過(NSString*) NSStringFromSelector(SEL)函數來擷取SEL 所指定的方法名稱
字串。
其實Objective-C 在編譯的時候,會依據每一個定義的方法的名字、參數序列,產生一個唯
一的整數標識,這個標識就是SEL。因此,在運行時尋找方法都是通過這個唯一的標識,而
不是通過方法的名字。
Objective-C 又提供了IMP 類型,IMP 表示指向實現方法的指標(函數指標),通過它,你可
以直接存取一個實現方法,從而避免了[xxx message]的靜態調用方式,需要首先通過SEL 確
定方法,然後再通過IMP 找到具體的實現方法,最後再發送訊息所帶來的執行效率問題。
一般,如果你在多次迴圈中反覆調用一個方法,用IMP 的方式,會比直接向對象發送訊息
高效一些。
例:
Person.m:
#import "Person.h"
@implementation Person
@synthesize name;
@synthesize weight;
-(Person*) initWithWeight: (int) w{
self=[super init];
if (self) {
weight=w;
}
return self;
}
-(void) print: (NSString*) str{
NSLog(@"%@ %@",str,name);
}
-(void) dealloc{
[self setName:nil];
[super dealloc];
}
@end
main.m:
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; Person *person=[[Person alloc] initWithWeight:68];
[email protected]"Jetta";
SEL print_sel=NSSelectorFromString(@"print:");
IMP imp=[person methodForSelector: print_sel];
imp(person,print_sel,@"*********");
[pool drain];
return 0;
}
這裡我們看到要獲得IMP 的指標,可以通過NSObject 中的methodForSelector: (SEL)方法,訪
問這個指標函數,我們使用imp(id,SEL,argument1,… …),第一個參數是調用方法的對象,第
二個方法是方法的選取器對象,第三個參數是可變參數,表示傳遞方法需要的參數。
(3.)objc_msgSend函數:
通過isa 指標的講解,我們知道Objective-C 中的方法調用是在運行時才去綁定的,再進一步
看,編譯器會把對象訊息發送[xxx method]轉換為objc_msgSend(id receiver,SEL selector,參數…)
的函數調用。因此上面例子中的print 方法你也可以像下面這樣調用:
objc_msgSend(person,print_sel,@"++++++++");
當然,這是編譯器要做的事情,你在寫代碼的時候,是不需要直接使用這種寫法的。
綜合isa、SEL、IMP 的講解,實際上objc_msgSend 的調用過程就應該是這樣的:
A.首先通過第一個參數的receiver,找到它的isa 指標,然後在isa 指向的Class 對象中使用
第二個參數selector 尋找方法;
B.如果沒有找到,就使用當前Class 對象中的新的isa 指標到上一級的父類的Class 對象中查
找;
C.當找到方法後,再依據receiver 的中的self 指標找到當前的對象,調用當前對象的具體實
現的方法(IMP 指標函數),然後傳遞參數,調用實現方法。
D.假如一直找到NSObject 的Class 對象,也沒有找到你調用的方法,就會報告不能識別發送
訊息的錯誤。
(4.)動態方法解析:
我們在Objective-C 2.0 的新特性中的屬性訪問器一節中,實際忽略了一個內容,那就是動態
屬性。Objective-C 2.0 中增加了@dynamic 指令,表示變數對應的屬性訪問器方法,是動態實
現的,你需要在NSObject 中繼承而來的+(BOOL) resolveInstanceMethod:(SEL) sel 方法中指定
動態實現的方法或者函數。
例:
Person.h:
@interface Person : NSObject{
NSString *name;
float weight;
}@property (retain,readwrite) NSString* name;
@property (readonly)float weight;
@property float height;-(Person*) initWithWeight: (int) weight;
-(void) print: (NSString*) str;
@end
Person.m:
void dynamicMethod(id self,SEL _cmd,float w){
printf("dynamicMethod-%s\n",[NSStringFromSelector(_cmd) cStringUsingEncoding:NSUTF8StringEncoding]);
printf("%f\n",w);
}
@implementation Person
@synthesize name;
@synthesize weight;
@dynamic height; // 注意這裡 // 在實作類別中使用了@dynamic指令
-(Person*) initWithWeight: (int) w{
self=[super init];
if (self) {
weight=w;
}
return self;
}
-(void) print: (NSString*) str{
NSLog(@"%@%@",str,name);
}
+(BOOL) resolveInstanceMethod: (SEL) sel{
NSString *methodName=NSStringFromSelector(sel);
BOOL result=NO;
//看看是不是我們要動態實現的方法名稱
if ([methodName isEqualToString:@"setHeight:"]) {
class_addMethod([self class], sel, (IMP) dynamicMethod,"[email protected]:f");
result=YES;
}
return result;
}
-(void) dealloc{
[self setName:nil];
[super dealloc];
}
@end 這裡我們對於介面中的height在實作類別中使用了@dynamic指令,緊接著,你需要指定一個函
數或者其他類的方法作為height的setter、getter方法的運行時實現。為了簡單,我們指定
了Person.m中定義的函數(注意這是C語言的函數,不是Objective-C的方法)dynamicMethod
作為height的setter方法的運行時實現。被指定為動態實現的方法的dynamicMethod的參數
有如下的要求:
A.第一個、第二個參數必須是id、SEL;
B.第三個參數開始,你可以按照原方法(例如:setHeight:(float))的參數定義。
再接下來,你需要覆蓋NSObject 的類方法resolveInstanceMethod,這個方法會把需要動態
實現的方法(setHeight:)的選取器傳遞進來,我們判斷一下是否是需要動態實現的選取器,
如果是就把處理權轉交給dynamicMethod。如何轉交呢?這裡我們就要用到運行時函數
class_addMethod(Class,SEL,IMP,char[])。
運行時函數位於objc/runtime.h,正如名字一樣,這裡面都是C 語言的函數。按照這些函數
的功能的不同,主要分為如下幾類:操作類型、操作對象、操作協議等。大多數的函數都可
以通過名字看出是什麼意思,例如:class_addProtocol 動態為一個類型在運行時增加協議、
objc_getProtocol 把一個字串轉換為協議等。具體這些運行時函數都是做什麼用的,你可
以參看Apple 官方頁面:
http://developer.apple.com/library/ios/documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html#//apple_ref/doc/uid/TP40001418
言歸正傳,我們來解釋一下這裡需要用到的class_addmethod 方法,這個方法有四個參數,
Class 表示你要為哪個類型增加方法,SEL 參數表示你要增加的方法的選取器,IMP 表示你要
添加的方法的運行時的具體實現的函數指標。其實在這裡你能夠看出SEL 並不能在運行時找
到真正要調用的方法,IMP 才可以真正的找到實現方法的。
在講解第四個參數char[]之前,我們先看一下第一篇文檔中提到的@encode 指令,在把任意
非Objective-C 物件類型封裝為NSValue 類型的時候使用到了@encode 指令,但當時我們沒
有詳細說明這個指令的含義。實際上@encode()可以接受任何類型,Objective-C 中用這個指
令做類型編碼,它可以把任何一個類型轉換為字串,譬如:void 類型被編碼之後為v,對
象類型為@,SEL 類型為:等,具體的你可以參看Apple 官方頁面關於Type Encoding 的描述:
http://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW
現在我們來正式的看以下第四個參數[email protected]:f 的含義,它描述了IMP 指向的函數的描述資訊,
按照@encode 指令編譯之後的字元說明,第一個字元v 表示傳回值為void,剩餘的字元為
dynamicMethod 函數的參數描述,@表示第一個參數id,:自然就是第二個參數SEL,f 就是
第三個參數float。由於前面說過動態方法的實現的前兩個參數必須是id、SEL,所以第四個
參數中的字串的第二、三個字元一定是@:。
我們看到resolveInstanceMethod 方法的傳回值為BOOL,也就是這個方法返回YES 表示找到
了動態方法的具體實現,否則就表示沒有在運行時找到真實的實現,程式就彙報錯。
經過了上面的處理,Objective-C 的運行時只要發現你調用了@dynamic 標註的屬性的setter、
getter 方法,就會自動到resolveInstanceMethod 裡去尋找真實的實現。這也就是說你在
main.m 中調用peson.height 的時候,實際上dynamicMethod 函數被調用了。
實際上除了@dynamic 標註的屬性之外,如果你調用了類型中不存在的方法,也會被
resolveInstanceMethod 或者resolveClassMethod 截獲,但由於你沒有處理,所以會報告不能
識別的訊息的錯誤。
你可能在感歎一個@dynamic 指令用起來真是麻煩,我也是研究了半天Apple 官方的晦澀的
鳥語才搞明白的。不過好在一般Objective-C 的運行時編程用到的並不多,除非你想設計一
個動態化的功能,譬如:從網路下載一個升級包,不需要退出原有的程式,就可以動態替
換掉舊的功能等類似的需求。
(5.)訊息轉寄:
在前面的objc_msgSend()函數的最後,我們總結了Objective-C 的方法調用過程,在最後一步
我們說如果一路找下來還是沒有找到調用的方法,就會報告錯誤,實際上這裡有個細節,那
就是最終找不到調用的方法的時候,系統會調用-(void) forwardInvocation: (NSInvocation*)
invocation 方法,如果你的對象沒有實現這個方法,就調用NSObject 的forwardInvocation 方
法,那句不能識別訊息的錯誤,實際就是NSObject 的forwardInvocation 拋出來的異常。
我們這裡告訴你這個系統內部的實現過程,實際是要告訴你,你可以覆蓋forwardInvocation
方法,來改變NSObject 的拋異常的處理方式。譬如:你可以把A 不能處理的訊息轉寄給B
去處理。
NSInvocation 是一個包含了receiver、selector 的對象,也就是它包含了向一個對象發送訊息
的所有元素:對象、方法名、參數序列,你可以調用NSInvocation 的invoke 方法將這個訊息
啟用。
例:
main.m:
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Person *person=[[Person alloc] init];
[email protected]"Jetta";
[person fly];
[person release];
[pool drain];
return 0;
}
這裡我們調用了一個Person 中不存在的方法fly。
Bird.m:
#import "Bird.h"
@implementation Bird
-(void) fly{
printf("Bird Can fly!");
}
@end
Person.m
@implementation Person
@synthesize name;
@synthesize weight;
-(NSMethodSignature*) methodSignatureForSelector:(SEL)selector{
//首先調用父類的方法
NSMethodSignature *signature=
[super methodSignatureForSelector: selector];
//如果當前對象無法回應此selector,那麼selector構造的方法簽名必然為nil
if (!signature) {
//首先判斷Bird的執行個體是否有能力回應此selector
if ([Bird instancesRespondToSelector:selector]) {
//擷取Bird的selector的方法簽名對象
signature=[Bird instanceMethodSignatureForSelector:selector];
}
}
return signature;
}
-(void) forwardInvocation: (NSInvocation*) invocation{
//首先驗證Bird是否有能力回應invocation中包含的selector
if ([Bird instancesRespondToSelector:[invocation selector]]) {
//建立要移交訊息響應權的執行個體bird
Bird *bird=[Bird new];
//啟用invocation中的訊息,但是訊息的響應者是bird,而不是預設的self。
[invocation invokeWithTarget:bird];
}
}
-(void) dealloc{
[self setName:nil];
[super dealloc];
}
@end 下面我們來詳細分析一下如果你想把不能處理的訊息轉寄給其他的對象,需要經過哪個幾個
步驟:A.首先,你要覆蓋NSObject中的methodSignatureForSelector方法。這是因為你如果想把消
息fly從Person轉寄給Bird處理,那麼你必須將NSInvocation中包含的Person的fly的方法簽
名轉換為Bird的fly的方法簽名,也就是把方法簽名糾正一下。
由此,你也看出來NSInvocation的建立,內部使用了兩個對象,一個是receiver,一個是
NSMethodSignature,而NSMethodSignature是由SEL建立的。NSInvocation確實存在一個類方
法invocationWithMethodSignature返回自身的執行個體。
B.然後我們覆蓋forwardInvocation方法,使用的不是invoke方法,而是invokeWithTarget方法,
也就是把調用權由self轉交給bird。
實際上訊息轉寄機制不僅可以用來處理找不到方法的錯誤,你還可以變相的實現多繼承。假
如我們的Person 想要擁有Bird、Fish 的所有功能,其實你可以盡情的用Person 的執行個體調用
Bird、Fish 的方法,只要在Person 的forwardInvocation 裡,把訊息的響應權轉交給Bird 或者
Fish 的執行個體就可以了。不過這種做法實在有點兒BT,除非萬不得已,否則千萬不要這麼做,
但是你也從這裡能夠看出來Objective-C 這種語言有多麼的靈活、強大,這是JAVA 所完全不
能相比的。
Objective-C 2.0的運行時編程