zzz Objective-C的訊息傳遞機制

來源:互聯網
上載者:User

標籤:os   io   使用   ar   for   檔案   資料   art   cti   

各種語言都有些傳遞函數的方法:C語言中可以使用函數指標,C++中有函數引用、仿函數和lambda,Objective-C裡也有選取器(selector)和block。
不過由於iOS SDK中的大部分API都是selector的方式,所以本文就重點講述selector了。

Objective-C和我接觸過的其他物件導向的語言不同,它強調訊息傳遞,而非方法調用。因此你可以對一個對象傳遞任何訊息,而不需要在編譯期聲名這些訊息的處理方法。
很顯然,既然編譯期並不能確定方法的地址,那麼運行期就需要自行定位了。而Objective-C runtime就是通過“id objc_msgSend(id theReceiver, SEL theSelector, ...)”這個函數來調用方法的。其中theReceiver是調用對象,theSelector則是訊息名,省略符號就是C語言的不定參數了。
這裡的訊息名是SEL類型,它被定義為struct objc_selector *。不過文檔中並沒有透露objc_selector是什麼東西,但提供了@selector指令來產生:

SEL selector = @selector(message);

@selector是在編譯期計算的,所以並不是函數調用。更進一步的測試表明,它在Mac OS X 10.6和iOS下都是一個C風格的字串(char*):

NSLog (@"%s", (char *)selector);

你會發現結果是“message”這個訊息名。

下面就寫個測試類別:

@interface Test : NSObject@end@implementation Test- (NSString *)intToString:(NSInteger)number {    return [NSString stringWithFormat:@"%d", number];}- (NSString *)doubleToString:(double *)number {    return [NSString stringWithFormat:@"%f", *number];}- (NSString *)pointToString:(CGPoint)point {    return [NSString stringWithFormat:@"{%f, %f}", point.x, point.y];}- (NSString *)intsToString:(NSInteger)number1 second:(NSInteger)number2 third:(NSInteger)number3 {    return [NSString stringWithFormat:@"%d, %d, %d", number1, number2, number3];}- (NSString *)doublesToString:(double)number1 second:(double)number2 third:(double)number3 {    return [NSString stringWithFormat:@"%f, %f, %f", number1, number2, number3];}- (NSString *)combineString:(NSString *)string1 withSecond:string2 withThird:string3 {    return [NSString stringWithFormat:@"%@, %@, %@", string1, string2, string3];}@end

再來測試下objc_msgSend:

#import <objc/message.h>//要使用objc_msgSend的話,就要引入這個標頭檔Test *test = [[Test alloc] init];CGPoint point = {123, 456};NSLog(@"%@", objc_msgSend(test, @selector(pointToString:), point));[test release];

結果是“{123.000000, 456.000000}”。而且與之前猜想的一樣,下面這樣調用也是可以的:

NSLog(@"%@", objc_msgSend(test, (SEL)"pointToString:", point));


看到這裡你應該發現了,這種實現方式只能確定訊息名和參數數目,而參數類型和傳回型別就給抹殺了。所以編譯器只能在編譯期警告你參數類型不對,而無法阻止你傳遞類型錯誤的參數。

接下來再看看NSObject協議提供的一些傳遞訊息的方法:

  • - (id)performSelector:(SEL)aSelector
  • - (id)performSelector:(SEL)aSelector withObject:(id)anObject
  • - (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

也沒有覺得很無語?為什麼參數必須是對象?為什麼最多隻支援2個參數?

好在selector本身也不在乎參數類型,所以傳個不是對象的玩意也行:

NSLog(@"%@", [test performSelector:@selector(intToString:) withObject:(id)123]);


可是double和struct就不能這樣傳遞了,因為它們占的位元組數和指標不一樣。如果非要用performSelector的話,就只能修改參數類型為指標了:

- (NSString *)doubleToString:(double *)number {    return [NSString stringWithFormat:@"%f", *number];}double number = 123.456;NSLog(@"%@", [test performSelector:@selector(doubleToString:) withObject:(id)(&number)]);


參數類型算是搞定了,可是要支援多個參數,還得費番氣力。理想狀態下,我們應該可以實現這2個方法:

@interface NSObject (extend)- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects;- (id)performSelector:(SEL)aSelector withParameters:(void *)firstParameter, ...;@end


先看看前者,NSArray要求所有的元素都必須是對象,並且不能為nil,所以適用的範圍仍然有限。不過你可別小看它,因為你會發現根本沒法用objc_msgSend來實現,因為你在寫代碼時沒法預知參數個數。
這時候就輪到NSInvocation登場了:

@implementation NSObject (extend)- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects {    NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];    [invocation setTarget:self];    [invocation setSelector:aSelector];        NSUInteger i = 1;    for (id object in objects) {        [invocation setArgument:&object atIndex:++i];    }    [invocation invoke];        if ([signature methodReturnLength]) {        id data;        [invocation getReturnValue:&data];        return data;    }    return nil;}@endNSLog(@"%@", [test performSelector:@selector(combineString:withSecond:withThird:) withObjects:[NSArray arrayWithObjects:@"1", @"2", @"3", nil]]);

這裡有3點要注意的:

  1. 因為方法調用有self(調用對象)和_cmd(選取器)這2個隱含參數,因此設定參數時,索引應該從2開始。
  2. 因為參數是對象,所以必須傳遞指標,即&object。
  3. methodReturnLength為0時,表明傳回型別是void,因此不需要擷取傳回值。傳回值是對象的情況下,不需要我們來建立buffer。但如果是C風格的字串、數組等類型,就需要自行malloc,並釋放記憶體了。


再來實現第2個方法:

- (id)performSelector:(SEL)aSelector withParameters:(void *)firstParameter, ... {    NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];    NSUInteger length = [signature numberOfArguments];    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];    [invocation setTarget:self];    [invocation setSelector:aSelector];        [invocation setArgument:&firstParameter atIndex:2];    va_list arg_ptr;    va_start(arg_ptr, firstParameter);    for (NSUInteger i = 3; i < length; ++i) {        void *parameter = va_arg(arg_ptr, void *);        [invocation setArgument:&parameter atIndex:i];    }    va_end(arg_ptr);        [invocation invoke];        if ([signature methodReturnLength]) {        id data;        [invocation getReturnValue:&data];        return data;    }    return nil;}NSLog(@"%@", [test performSelector:@selector(combineString:withSecond:withThird:) withParameters:@"1", @"2", @"3"]);NSInteger number1 = 1, number2 = 2, number3 = 3;NSLog(@"%@", [test performSelector:@selector(intsToString:second:third:) withParameters:number1, number2, number3]);

和前面的實現差不多,不過由於參數長度是未知的,所以用到了[signature numberOfArguments]。當然也可以把SEL轉成字串(可用NSStringFromSelector()),然後尋找:的數量。
處理可變參數時用到了va_start、va_arg和va_end,熟悉C語言的一看就明白了。
不過由於不知道參數的類型,所以只能設為void *。而這個程式也報出了警告,說void *和NSInteger類型不相容。而如果把參數換成double,那就直接報錯了。遺憾的是我也不知道怎麼判別一個void *指標究竟是指向C資料類型,還是指向一個Objective-C對象,所以最好是封裝成Objective-C對象。如果只需要相容C類型的話,倒是可以將setArgument的參數的&去掉,然後直接傳指標進去:

NSInteger number1 = 1, number2 = 2, number3 = 3;NSLog(@"%@", [test performSelector:@selector(intsToString:second:third:) withParameters:&number1, &number2, &number3]);double number4 = 1.0, number5 = 2.0, number6 = 3.0;NSLog(@"%@", [test performSelector:@selector(doublesToString:second:third:) withParameters:&number4, &number5, &number6]);[test release];

至於NSObject類添加的performSelector:withObject:afterDelay:等方法,也可以用這種方式來支援多個參數。

接下來再說說剛才略過的_cmd,它還可以用來實現遞迴調用。下面就以斐波那契數列為例:

- (NSInteger)fibonacci:(NSInteger)n {    if (n > 2) {        return [self fibonacci:n - 1] + [self fibonacci:n - 2];    }    return n > 0 ? 1 : 0;}

改成用_cmd實現就變成了這樣:

return (NSInteger)[self performSelector:_cmd withObject:(id)(n - 1)] + (NSInteger)[self performSelector:_cmd withObject:(id)(n - 2)];

或者直接用objc_msgSend:

return (NSInteger)objc_msgSend(self, _cmd, n - 1) + (NSInteger)objc_msgSend(self, _cmd, n - 2);


但是每次都通過objc_msgSend來調用顯得很費勁,有沒有辦法直接進行方法調用呢?答案是有的,這就需要用到IMP了。IMP的定義為“id (*IMP) (id, SEL, …)”,也就是一個指向方法的函數指標。
NSObject提供methodForSelector:方法來擷取IMP,因此只需稍作修改就行了:

- (NSInteger)fibonacci:(NSInteger)n {    static IMP func;    if (!func) {        func = [self methodForSelector:_cmd];    }        if (n > 2) {        return (NSInteger)func(self, _cmd, n - 1) + (NSInteger)func(self, _cmd, n - 2);    }    return n > 0 ? 1 : 0;}

現在已耗用時間比剛才減少了1/4,還算不錯。

順便再展現一下Objective-C強大的動態性,給Test類添加一個sum:and:方法:

NSInteger sum(id self, SEL _cmd, NSInteger number1, NSInteger number2) {    return number1 + number2;}class_addMethod([Test class], @selector(sum:and:), (IMP)sum, "[email protected]:ii");NSLog(@"%d", [test sum:1 and:2]);

zzz Objective-C的訊息傳遞機制

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.