如何在Cocoa中使用performSelector:withObject:afterDelay:处理原始数据类型?

97
NSObjectperformSelector:withObject:afterDelay:方法允许我在一定时间后调用带有对象参数的对象的方法。它不能用于带有非对象参数(例如int、float、struct、非对象指针等)的方法。

如何用一个带有非对象参数的方法实现同样的功能?我知道对于普通的performSelector:withObject:,解决方案是使用NSInvocation(顺便说一下,这真的很复杂)。但我不知道如何处理“延迟”部分。

谢谢,


这段代码有点hackish,但我发现它可以帮助编写快速的代码。类似这样:*id object= [array performSelector: @selector(objectAtIndex:) withObject: (__bridge_transfer)(void*)1];*。如果参数是0,甚至不需要桥接转换,因为0是“特殊”的,而且id与之兼容。 - Ramy Al Zuhouri
13个回答

72

这是我用来调用无法使用NSInvocation更改的内容的方法:

SEL theSelector = NSSelectorFromString(@"setOrientation:animated:");
NSInvocation *anInvocation = [NSInvocation
            invocationWithMethodSignature:
            [MPMoviePlayerController instanceMethodSignatureForSelector:theSelector]];

[anInvocation setSelector:theSelector];
[anInvocation setTarget:theMovie];
UIInterfaceOrientation val = UIInterfaceOrientationPortrait;
BOOL anim = NO;
[anInvocation setArgument:&val atIndex:2];
[anInvocation setArgument:&anim atIndex:3];

[anInvocation performSelector:@selector(invoke) withObject:nil afterDelay:1];

为什么不直接使用 [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO] 呢?或者你是指在延迟执行的方法中有 invoke 消息吗? - Peter Hosey
啊呀,我忘了说... [anInvocation performSelector:@selector(invoke) afterDelay:0.5]; - Morty
26
对于不了解的人,我需要说明一下,我们从索引2开始添加参数,因为索引0和1被保留用于隐藏参数“self”和“_cmd”。 - Vishal Singh

35

只需将浮点数、布尔值、整型或类似类型包装在NSNumber中。

对于结构体,我不知道有什么便捷的解决方案,但您可以创建一个独立的ObjC类来拥有这样的结构体。


5
创建一个包装方法,名为-delayedMethodWithArgument:(NSNumber*)arg。它在从NSNumber中提取出原始值后调用原始方法。 - James Williams
1
明白了。那我不确定是否有一个优雅的解决方案,但您可以添加另一种方法,该方法旨在成为performSelector:的目标,它会解包NSNumber并在您当前考虑的方法上执行最终选择器。您可以尝试的另一个想法是,在NSObject上创建一个类别,添加perforSelector:withInt:...(以及类似的内容)。 - harms
32
NSValue(NSNumber的超类)可以包装任何给定的内容。如果你想要包装一个名为“bar”的“struct foo”的实例,你可以使用 '[NSValue valueWithBytes: &bar objCType: @encode(struct foo)];'。 - Jim Dovey
2
您可以使用NSValue来包装一个结构体。无论哪种方式,NSNumber/NSValue都是正确的解决方案。 - Peter Hosey
6
确认:必须BOOL参数传递nil以接收NOFALSE)。 - Nicolas Miari
显示剩余4条评论

13

如果参数是BOOL类型,有一个简单的技巧。

传递nil表示NO,传递self表示YES。nil被转换为NO的BOOL值。self被转换为YES的BOOL值。

如果参数不是BOOL类型,则此方法失效。

假设self是一个UIView。

//nil will be cast to NO when the selector is performed
[self performSelector:@selector(setHidden:) withObject:nil afterDelay:5.0];

//self will be cast to YES when the selector is performed
[self performSelector:@selector(setHidden:) withObject:self afterDelay:10.0];

1
谢谢,我不想创建一个包装器或编写丑陋的GCD语法来完成这个。 - 0xSina
10
不要使用这种方法。这种方法是严重错误的根源,很难找到。想象一下self地址为0x0123400时会发生什么。使用这种方法,你将在0.4%的情况下得到NO而不是YES。更糟糕的是,有了这个概率,这种解决方案可能会通过所有测试,并在后来揭示问题。 - Oleg Trakhman
1
更新:由于内存对齐,有6%的概率会得到“NO”而不是“YES”。 - Oleg Trakhman
4
请查看BOOL的尖角 - Farray
@buildsucceeded,这仍然会让你面临内存对齐问题,因为你正在将NSNumber指针发送到setHidden,而不是它所包装的YES。 - Saltymule
显示剩余3条评论

8

我知道这是一个老问题,但如果你正在构建 iOS SDK 4+,那么你可以使用块来轻松实现并使代码更加易读:

double delayInSeconds = 2.0;
int primitiveValue = 500;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [self doSomethingWithPrimitive:primitiveValue];     
});

这会创建一个保留循环。为了使其正常工作,您需要对self进行弱引用,并使用该引用执行选择器。 "__block typeof(self) weakSelf = self;" - Tim Wachter
2
这里不需要弱引用,因为self不会保留block(没有保留循环)。实际上最好使用强引用,否则可能会释放self。请参阅:https://dev59.com/v2Mk5IYBdhLWcg3wvARo#19018633 - Sebastien Martin

8
也许可以使用NSValue,但请确保在延迟之后您的指针仍然有效(即不要分配在堆栈上的对象)。

任何旧的方法是否可以(如果可能)将NSValue“解包”为预期的类型 - Alex Gray

6

PerformSelector:WithObject总是需要一个对象作为参数,所以如果想要传递像int/double/float等类型的参数......,您可以使用类似以下方式。

//NSNumber is an object..

    [self performSelector:@selector(setUserAlphaNumber:) withObject: [NSNumber numberWithFloat: 1.0f]
    afterDelay:1.5];

    -(void) setUserAlphaNumber: (NSNumber*) number{

     [txtUsername setAlpha: [number floatValue] ];
    }

同样的方式,你可以使用[NSNumber numberWithInt:]等...在接收方法中,你可以将数字转换为你的格式,如[number int]或[number double]。

1
如果只能使用+performSelector:withObject:+,那么在我看来,这是唯一正确的答案。但是@MichaelGaylord使用块更加简洁,如果您可以使用它的话。 - big_m

5

块是前进的方式。您可以具有复杂参数、类型安全性,而且它比大多数旧答案都要简单和更安全。例如,您可以只写如下代码:

[MONBlock performBlock:^{[obj setFrame:SOMETHING];} afterDelay:2];

块允许捕获任意参数列表,引用对象和变量。

后备实现(基本):

@interface MONBlock : NSObject

+ (void)performBlock:(void(^)())pBlock afterDelay:(NSTimeInterval)pDelay;

@end

@implementation MONBlock

+ (void)imp_performBlock:(void(^)())pBlock
{
 pBlock();
}

+ (void)performBlock:(void(^)())pBlock afterDelay:(NSTimeInterval)pDelay
{
  [self performSelector:@selector(imp_performBlock:)
             withObject:[pBlock copy]
             afterDelay:pDelay];
}

@end

例子:

int main(int argc, const char * argv[])
{
 @autoreleasepool {
  __block bool didPrint = false;
  int pi = 3; // close enough =p

  [MONBlock performBlock:^{NSLog(@"Hello, World! pi is %i", pi); didPrint = true;} afterDelay:2];

  while (!didPrint) {
   [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeInterval:0.1 sinceDate:NSDate.date]];
  }

  NSLog(@"(Bye, World!)");
 }
 return 0;
}

另外可以看看迈克尔的回答(+1)作为另一个例子。


3

我建议您使用NSMutableArray作为传递对象。这是因为您可以传递多个对象,例如按下的按钮和其他值。NSNumber、NSInteger和NSString只是一些值的容器。确保从数组中获取对象时,引用正确的容器类型。您需要传递NS容器。在那里,您可以测试值。请记住,当比较值时,容器使用isEqual。

#define DELAY_TIME 5

-(void)changePlayerGameOnes:(UIButton*)sender{
    NSNumber *nextPlayer = [NSNumber numberWithInt:[gdata.currentPlayer intValue]+1 ];
    NSMutableArray *array = [[NSMutableArray alloc]initWithObjects:sender, nil];
    [array addObject:nextPlayer];
    [self performSelector:@selector(next:) withObject:array afterDelay:DELAY_TIME];
}
-(void)next:(NSMutableArray*)nextPlayer{
    if(gdata != nil){ //if game choose next player
       [self nextPlayer:[nextPlayer objectAtIndex:1] button:[nextPlayer objectAtIndex:0]];
    }
}

2

我也想用一种接收BOOL参数的方法来实现这个,但是用NSNumber包装bool值后,没能成功传递该值。我不知道为什么会出现这种情况。

所以我最终采取了一个简单的hack方法。我将所需的参数放在另一个虚拟函数中,并使用performSelector调用该函数,其中withObject = nil;

[self performSelector:@selector(dummyCaller:) withObject:nil afterDelay:5.0];

-(void)dummyCaller {

[self myFunction:YES];

}

请看我对harms答案的评论。看起来,由于-performSelector[...]方法期望一个对象(而不是原始数据类型),并且它不知道所调用的选择器接受布尔值,也许NSNumber实例的地址(指针值)被盲目地转换为BOOL(非零,即TRUE)。也许运行时可以比这更聪明,并识别包装在NSNumber中的零布尔值! - Nicolas Miari
我同意。我想更好的做法是完全避免使用BOOL,改用整数0表示NO,1表示YES,并将其包装在NSNumber或NSValue中。 - GeneCode
这会改变什么吗?如果是这样,突然间我对Cocoa感到非常失望 :) - Nicolas Miari

2

我发现最快(但有点不规范)的方法是直接调用objc_msgSend。然而,直接调用它是很危险的,因为您需要阅读文档并确保使用正确的变体来处理返回值类型,而且objc_msgSend被定义为vararg以方便编译器,但实际上是由快速的汇编胶水实现的。下面是一些用于调用代理方法-[delegate integerDidChange:]的代码,该方法需要一个整数参数。

#import <objc/message.h>


SEL theSelector = @selector(integerDidChange:);
if ([self.delegate respondsToSelector:theSelector])
{
    typedef void (*IntegerDidChangeFuncPtrType)(id, SEL, NSInteger);
    IntegerDidChangeFuncPtrType MyFunction = (IntegerDidChangeFuncPtrType)objc_msgSend;
    MyFunction(self.delegate, theSelector, theIntegerThatChanged);
}

首先,我们需要保存选择器,因为我们会多次引用它,如果没有保存可能会出现拼写错误。接下来,我们验证委托是否实际响应了该选择器 - 它可能是一个可选协议。然后,创建一个函数指针类型,指定选择器的实际签名。请记住,所有的Objective-C消息都有两个隐藏的第一个参数,即被发送消息的对象和被发送的选择器。接着,我们创建一个适当类型的函数指针,并将其设置为指向底层的objc_msgSend函数。请记住,如果返回值是浮点数或结构体,则需要使用objc_msgSend的不同变体。最后,使用与Objective-C在底层使用的相同机制发送消息。


请记住,如果您正在发送浮点数或结构体,则意味着返回浮点数或结构体。 - user102008
这三行代码可以确保类型安全,并保证参数数量与选择器所期望的相符。objc_msgSend是一个可变参数函数,实际上是作为汇编粘合剂实现的,因此如果您不进行类型转换,可以传递任何内容。 - Jason Harris

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