Objective-C 2.0的運行時編程

原文地址:http://www.cnblogs.com/tangbinblog/archive/2012/11/08/2760060.html


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];
  person.name=@"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,"v@: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


  現在我們來正式的看以下第四個參數v@: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];
  person.name=@"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 所完全不
能相比的


發佈了11 篇原創文章 · 獲贊 2 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章