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 所完全不
能相比的。