NSInvocation是什么?

141

NSInvocation 究竟是如何工作的?有没有好的介绍?

我特别困扰于理解以下代码(来自 Cocoa Programming for Mac OS X, 3rd Edition),但我也想能够独立应用这些概念而不仅仅是按照教程样例来编写代码。代码如下:

- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index
{
    NSLog(@"adding %@ to %@", p, employees);
    // Add inverse of this operation to undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self] removeObjectFromEmployeesAtIndex:index];
    if (![undo isUndoing])
        [undo setActionName:@"Insert Person"];
    
    // Finally, add person to the array
    [employees insertObject:p atIndex:index];
}

- (void)removeObjectFromEmployeesAtIndex:(int)index
{
    Person *p = [employees objectAtIndex:index];
    NSLog(@"removing %@ from %@", p, employees);
    // Add inverse of this operation to undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self] insertObject:p
                                       inEmployeesAtIndex:index];
    if (![undo isUndoing])
        [undo setActionName:@"Delete Person"];
    
    // Finally, remove person from array
    [employees removeObjectAtIndex:index];
}

我理解它的意思。(顺便说一下,employees是一个自定义的Person类的NSArray。)

作为.NET开发者,我试图将不熟悉的Obj-C和Cocoa概念与大致类似的.NET概念联系起来。这是否类似于.NET的委托(delegate)概念,但没有类型限制?

从书本上来看这并不是百分之百清晰的,所以我在寻找真正的Cocoa/Obj-C专家的补充材料,目的是我能够理解简单(或相对简单)示例背后的基本概念。我真的希望能够独立地应用这些知识——直到第9章,我都没有遇到任何困难。但现在…

4个回答

290
根据苹果的NSInvocation类参考文档

NSInvocation是一个Objective-C消息,它被呈现为静态形式,也就是说,它是一个被转换成对象的操作。

并且,稍微详细一点:

消息的概念是Objective-C哲学的核心。每当您调用某个对象的方法或访问其变量时,都会向其发送消息。当您想要在不同的时间向对象发送消息或多次发送相同的消息时,NSInvocation非常有用。 NSInvocation允许您描述您将要发送的消息,然后稍后(实际上将其发送到目标对象)调用它。


例如,假设您想将字符串添加到数组中。您通常会发送以下addObject:消息:
[myArray addObject:myString];

现在,假设您想在其他时间点使用NSInvocation发送此消息:
首先,您需要准备一个NSInvocation对象,以便与NSMutableArray的addObject:选择器一起使用:
NSMethodSignature * mySignature = [NSMutableArray
    instanceMethodSignatureForSelector:@selector(addObject:)];
NSInvocation * myInvocation = [NSInvocation
    invocationWithMethodSignature:mySignature];

接下来,您需要指定要发送消息的对象:

[myInvocation setTarget:myArray];

请指定您要发送到该对象的消息:

[myInvocation setSelector:@selector(addObject:)];

并填写该方法的任何参数:

[myInvocation setArgument:&myString atIndex:2];

注意,对象参数必须通过指针传递。感谢Ryan McCuaig指出这一点,请参阅Apple的文档了解更多细节。
此时,myInvocation是一个完整的对象,描述可以发送的消息。要实际发送消息,您需要调用:
[myInvocation invoke];

这最后一步将导致消息被发送,实际上执行了[myArray addObject:myString];
可以将其类比为发送电子邮件。您打开新的电子邮件(NSInvocation对象),填写收件人的地址(要发送到的对象),为收件人输入消息(指定一个selector和参数),然后单击“发送”(调用invoke)。
有关更多信息,请参见使用NSInvocation。 如果上述方法无效,请参见使用NSInvocation

NSUndoManager使用NSInvocation对象以便可以撤销命令。基本上,您正在创建一个NSInvocation对象来表明:“嘿,如果您想撤消我刚才所做的操作,请向该对象发送此消息,并使用这些参数”。您将NSInvocation对象提供给NSUndoManager,它会将该对象添加到可撤销操作的数组中。如果用户调用“撤消”,NSUndoManager只需在数组中查找最近的操作,并调用存储的NSInvocation对象执行必要的操作。

有关更多详细信息,请参见注册撤销操作


10
在这个非常好的回答中只有一个小错误需要更正......在setArgument:atIndex:中你需要传递对象的指针,所以参数赋值实际上应该写成 [myInvocation setArgument:&myString atIndex:2] - Ryan McCuaig
62
为了澄清Ryan的笔记,索引0保留给“self”,索引1保留给“_cmd”(有关详细信息,请参见e.James发布的链接)。因此,您的第一个参数放置在索引2处,第二个参数放置在索引3处,依此类推... - Dave
4
@haroldcampbell: 我们需要打电话吗? - e.James
6
我不明白为什么我们需要调用setSelector,因为我们已经在mySignature中指定了选择器。 - Gleno
6
@Gleno:NSInvocation非常灵活。 实际上,您可以设置与方法签名匹配的任何选择器,因此您不一定必须使用用于创建方法签名的相同选择器。 在这个例子中,您可以轻松地使用setSelector:@selector(removeObject:),因为它们共享相同的方法签名。 - e.James
显示剩余9条评论

50

这里是使用NSInvocation的简单示例:

- (void)hello:(NSString *)hello world:(NSString *)world
{
    NSLog(@"%@ %@!", hello, world);

    NSMethodSignature *signature  = [self methodSignatureForSelector:_cmd];
    NSInvocation      *invocation = [NSInvocation invocationWithMethodSignature:signature];

    [invocation setTarget:self];                    // index 0 (hidden)
    [invocation setSelector:_cmd];                  // index 1 (hidden)
    [invocation setArgument:&hello atIndex:2];      // index 2
    [invocation setArgument:&world atIndex:3];      // index 3

    // NSTimer's always retain invocation arguments due to their firing delay. Release will occur when the timer invalidates itself.
    [NSTimer scheduledTimerWithTimeInterval:1 invocation:invocation repeats:NO];
}

当调用 - [self hello:@"Hello" world:@"world"]; - 方法时,该方法将会:

  • 打印出 "Hello world!"
  • 为自身创建一个 NSMethodSignature。
  • 创建并填充一个 NSInvocation,调用自身。
  • 将 NSInvocation 传递给 NSTimer。
  • 计时器将在(大约)1秒后触发,导致使用原始参数再次调用该方法。
  • 重复执行。

最终,您将获得如下的输出:

2010-07-11 17:48:45.262 Your App[2523:a0f] Hello world!
2010-07-11 17:48:46.266 Your App[2523:a0f] Hello world!
2010-07-11 17:48:47.266 Your App[2523:a0f] Hello world!
2010-07-11 17:48:48.267 Your App[2523:a0f] Hello world!
2010-07-11 17:48:49.268 Your App[2523:a0f] Hello world!
2010-07-11 17:48:50.268 Your App[2523:a0f] Hello world!
2010-07-11 17:48:51.269 Your App[2523:a0f] Hello world!
...

当然,目标对象self必须继续存在,以便NSTimer将NSInvocation发送到该对象。例如,单例对象或在应用程序持续时间内存在的AppDelegate。


更新:

如上所述,当您将NSInvocation作为NSTimer的参数传递时,NSTimer会自动保留所有NSInvocation的参数。

如果您没有将NSInvocation作为NSTimer的参数传递,并计划让它长时间保留,则必须调用其 -retainArguments 方法。否则,在调用调用之前,它的参数可能已被释放,最终导致代码崩溃。以下是如何做到这一点:

NSMethodSignature *signature  = ...;
NSInvocation      *invocation = [NSInvocation invocationWithMethodSignature:signature];
id                arg1        = ...;
id                arg2        = ...;

[invocation setTarget:...];
[invocation setSelector:...];
[invocation setArgument:&arg1 atIndex:2];
[invocation setArgument:&arg2 atIndex:3];

[invocation retainArguments];  // If you do not call this, arg1 and arg2 might be deallocated.

[self someMethodThatInvokesYourInvocationEventually:invocation];

6
有趣的是,即使使用了invocationWithMethodSignature:初始化程序,你仍然需要调用setSelector:。这看起来是多余的,但我刚刚测试过,是必要的。 - ThomasW
这个程序会一直运行吗?_cmd是什么? - j2emanue

6

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接