oc中消息传递机制-附:对performSelector方法的扩充

各种语言都有些传递函数的方法: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 = {123456};
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;
}

@end


NSLog(@"%@", [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, "i@:ii");
NSLog(@"%d", [test sum:1 and:2]);

class_addMethod的最后那个参数是函数的返回值和参数类型,详细内容可以参考Type Encodings文档。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Objective-C的forward按钮代表了一个向前的按钮,用于在视图控制器进行导航操作。在iOS应用程序,导航栏通常使用导航控制器来管理多个视图控制器之间的导航流程。forward按钮是导航栏上的一个图标按钮,当用户点击该按钮时,它会在导航控制器将当前视图控制器推出栈并显示下一个视图控制器。 通过forward按钮,用户可以在应用程序的不同页面之间进行快速切换和导航操作。这方便了用户在应用程序进行不同页面的浏览和交互。例如,在一个新闻应用程序,forward按钮可以用于在不同的新闻文章之间进行切换。用户可以通过点击forward按钮轻松地浏览不同的新闻文章,而不需要返回到列表页面再选择下一篇文章。 在Objective-C,使用forward按钮需要在对应的视图控制器添加相应的事件处理方法。通过实现该事件处理方法,可以定义forward按钮点击后的具体导航操作或其他逻辑。例如,在事件处理方法可以使用导航控制器的pushViewController方法将下一个视图控制器推入栈并显示在屏幕上。 总之,Objective-C的forward按钮是一种用于导航控制器进行页面切换和导航操作的交互元素。它可以帮助用户快速切换页面,提升应用程序的用户体验和导航效率。 ### 回答2: Objective-C的forwardButton是一个用于处理按钮点击事件的方法,它允许开发者自定义按钮的行为和逻辑。 在Objective-C,我们通常使用UIButton类来创建和使用按钮。当用户点击按钮时,系统会调用按钮的事件处理方法,例如`buttonClicked:`。但是有时候我们可能需要在按钮点击事件被触发之前,先执行一些自定义的逻辑。这时就可以使用forwardButton方法。 forwardButton方法是一个用于处理按钮点击事件的方法。它被系统调用时,会首先执行一些自定义的逻辑,然后再转发(forward)按钮点击事件到相应的处理方法,例如`buttonClicked:`。这样就可以在按钮点击事件被处理前,先执行我们自己定义的一些逻辑。 使用forwardButton方法的步骤如下: 1. 在需要使用forwardButton方法的类,定义一个名为forwardButton的方法,并设置其参数为按钮对象。 2. 在forwardButton方法,编写自己需要执行的逻辑。这些逻辑可以是与按钮点击事件相关的任意操作,例如改变按钮的颜色、改变界面的显示等。 3. 在forwardButton方法,使用performSelector方法来调用相应的按钮处理方法,例如`[self performSelector:@selector(buttonClicked:) withObject:button]`。注意要确保按钮处理方法存在,并且与forwardButton方法的参数一致。 4. 在按钮点击事件发生时,系统会自动调用forwardButton方法,然后再转发到相应的按钮处理方法,实现我们的自定义逻辑。 总之,Objective-C的forwardButton方法提供了一种灵活的方式来自定义按钮的行为和逻辑。通过在forwardButton方法编写自己的代码,可以在按钮点击事件处理前执行一些自定义操作,从而实现更加个性化的按钮行为。 ### 回答3: Objective-C 的 forward button(前进按钮)是一种 UI 元素,用于在应用程序或浏览器导航到下一个页面、屏幕或视图。它通常显示为一个箭头图标,并放置在界面的左侧或右侧。 forward button 的主要作用是方便用户快速导航到下一个所需页面。用户可以点击该按钮,从当前页面跳转到下一个页面,从而加快应用程序或网页的使用效率。它尤其在浏览器经常使用,用户可以通过 forward button 快速返回到之前访问过的页面。 在 Objective-C ,可以通过以下方式来实现 forward button: 1. 创建 UIButton 实例,并设置按钮的图像为前进箭头图标。可以使用系统提供的箭头图标,也可以使用自定义的图标。 2. 将按钮添加到应用程序的视图层次结构,可以通过 addSubview 方法添加到父视图。 3. 为按钮添加一个点击事件,比如 addTarget:action:forControlEvents: 方法,当用户点击按钮时执行相应的操作。例如,可以使用 forward 方法导航到下一个页面或视图。 4. 根据需求,可以为按钮设置其他属性,如按钮的位置、大小、颜色等。 5. 在需要展示 forward button 的界面,将该按钮可见,对应的页面已提供返回功能的页面即可展示 forward button。 总之,Objective-C 的 forward button 是一种方便用户导航到下一个页面的 UI 元素。它可以帮助用户快速跳转到下一个所需的视图或页面,提高应用程序或网页的用户体验。通过使用 UIButton 创建、设置和处理点击事件,可以在 Objective-C 的应用程序轻松实现 forward button 功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值