使用[NSInvocation getReturnValue:]获取double值时,意外地产生了0

15

我正在尝试使用NSInvocation调用返回double的方法。但我发现在64位iOS应用程序中它不起作用。它在OS X上,模拟器上(包括32位和64位),iPad 2上以及使用32位构建的iPad Air上工作。只有在iPad Air设备上进行64位构建时才会出现此问题。

以下是演示该问题的代码:

NSMethodSignature *signature = [NSString instanceMethodSignatureForSelector:@selector(doubleValue)];
for (int i = 0; i < 10; i++) {
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    NSString *str = [NSString stringWithFormat:@"%d", i];
    [invocation setTarget:str];
    [invocation setSelector:@selector(doubleValue)];
    [invocation invoke];
    union {
        double d;
        uint64_t u;
    } d;
    [invocation getReturnValue:&d.d];
    NSLog(@"%lf, %llx", d.d, d.u);
}

预期输出

2013-11-09 22:34:18.645 test[49075:907] 0.000000, 0
2013-11-09 22:34:18.647 test[49075:907] 1.000000, 3ff0000000000000
2013-11-09 22:34:18.648 test[49075:907] 2.000000, 4000000000000000
2013-11-09 22:34:18.649 test[49075:907] 3.000000, 4008000000000000
2013-11-09 22:34:18.650 test[49075:907] 4.000000, 4010000000000000
2013-11-09 22:34:18.651 test[49075:907] 5.000000, 4014000000000000
2013-11-09 22:34:18.652 test[49075:907] 6.000000, 4018000000000000
2013-11-09 22:34:18.653 test[49075:907] 7.000000, 401c000000000000
2013-11-09 22:34:18.654 test[49075:907] 8.000000, 4020000000000000
2013-11-09 22:34:18.654 test[49075:907] 9.000000, 4022000000000000

iPad Air 64位构建的输出结果

2013-11-09 22:33:55.846 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.847 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.848 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.848 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.849 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.849 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.850 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.850 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.851 test[998:60b] 0.000000, 18710a969
2013-11-09 22:33:55.851 test[998:60b] 0.000000, 18710a969

浮点数值也会发生这种情况。

2013-11-09 23:51:10.021 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.023 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.024 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.024 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.025 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.026 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.026 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.027 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.027 test[1074:60b] -0.000000, 80000000
2013-11-09 23:51:10.028 test[1074:60b] -0.000000, 80000000

出于好奇,你能把[invocation setTarget:[NSString stringWithFormat:@"%d", i]];拆成两行吗?NSString *t=[NSString stringWithFormat:@"%d", i]; [invocation setTarget:t];?我想知道目标字符串是否会过早释放。 - Sergey Kalinichenko
我不知道问题出在哪里,但是如果double变量参数与long long变量参数的处理方式不同,那么你打印十六进制数的方法可能无法正常工作。最好定义一个union { double d; uint64_t x; } u;,并像这样使用它:NSLog(@"%lf, %llx", u.d, u.x); - Martin R
@dasblinkenlight:有趣的想法。如果是这样的话,[invocation retainArguments] 就可以解决它。 - jscs
@dasblinkenlight 更新了代码,但没有任何区别。我正在使用 ARC 和 [NSString stringWithFormat:] 方法返回自动释放的对象,所以这不会是问题。 - Bryan Chen
1
也许有人可以在iPhone 5s上检查它,因为它也可以运行64位代码。 - Martin R
显示剩余5条评论
4个回答

17

同意 @David H 的观点,即在此情况下 NSInvocation 已损坏或可能是NSString doubleValue 方法。不过我成功地将其强制执行。

在我看来,NSInvocation 由于调用约定问题或不匹配而无法正常工作。通常,objective-c 方法的参数和返回值会被传递到寄存器中。(objc_msgSend 知道如何执行这种类型的调用)。但是,如果参数或返回值是结构体或不适合寄存器的类型,则它们会在堆栈上传递.(objc_msgSend_stret 执行这种类型的调用)。NSInvocation 通常使用方法签名来判断是否需要调用objc_msgSendobjc_msgSendStret。我猜现在它还需要知道它正在运行的平台,这就是 bug 的所在之处。

我对您的代码进行了一些修改,似乎在 arm64 上,双倍返回值被传递为结构体,但是NSInvocation 将其视为在寄存器中传递。我不知道哪边是正确的。(我只知道在这个领域里我越懂越危险。我希望更有低级别技能的人看到这篇文章,并提供更好的解释!)

话虽如此,在我看来,arm(32位)与arm64中参数和结果传递方式存在重大变化。请参阅ARM Procedure Call Standard for arm64ARM Procedure Call Standard (非64位) 中的Result Return 部分。

我可以强制NSInvocation 将调用作为返回包含 double 的结构体处理,从而使其按预期工作。 为此,我将方法签名伪造为返回结构体的假签名。我将其放在了一个NSString类别中,但它也可以存在于任何地方。

由于不知道具体哪里出了问题,或者修复后会发生什么情况,我不太可能使用这个“修复”发布代码。我会找到其他解决方案。

typedef struct
{
    double d;

} doubleStruct;

@interface NSString (TS)
- (doubleStruct) ts_doubleStructValue;
@end
@implementation NSString (TS)
- (doubleStruct) ts_doubleStructValue
{
    doubleStruct ds;
    return ds;
}
@end


- (void) test
{
    NSMethodSignature *signature = [NSString instanceMethodSignatureForSelector: @selector( ts_doubleStructValue )];
    for (int i = 0; i < 10; i++) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        NSString *str = [NSString stringWithFormat:@"%d", i];
        [invocation setTarget:str];
        [invocation setSelector:@selector(doubleValue)];
        [invocation invoke];

        double d;
        [invocation getReturnValue: &d];

        NSLog(@"%lf", d);
    }
}

我认为我在使用NSInvocation时遇到了相同的问题,它返回一个void值,但是将一个使用NS_ENUM定义的NSInteger大小的枚举作为参数传递。我得到了EXC_BAD_ACCESS错误。所有调用都是同步的(没有异步调度),并且所有内容都被保留,因此不应该是内存问题。而且在我们的应用程序中,它已经在armv7(s)上工作了几年。:-P - stuckj
我在一台2010年中期的MBP上运行命令行应用程序时看到了相同的症状,因此似乎这不仅限于arm芯片。此外,调用使用int(64位)可以工作,但是使用double(也是64位)就无法工作,因此似乎这不是数据大小特定的问题。 - Dave Durbin

2
您目前没有希望使这个工作。我尝试了您的代码的许多变化,并在iPhone 5S和最新的模拟器中以32位和64位模式进行了测试,但这必须是arm64的错误,因为64位模拟器运行得很好。
所以,我首先修改了您的代码以尝试各种变化,结果使用floatValue也失败了。由于浮点数的大小是常见的,这减少了各种试验平台之间的变量数量。
此外,我尝试使用使用整数和浮点方法创建的NSNumber目标:浮点方法实际上会导致崩溃!我尝试了其他选项,如保留字符串并设置调用保留设置,但没有变化。
我已经输入了一个错误:15441447:NSInvocation仅在arm64设备上失败 - 任何关心它的人都可以复制它。我还上传了一个演示项目。
我上传的代码是:
- (void)test
{
    NSMethodSignature *signature = [NSString instanceMethodSignatureForSelector:@selector(floatValue)];
    for (int i = 0; i < 10; i++) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

#if 0
        NSString *str = [NSString stringWithFormat:@"%d", i];
        [invocation setTarget:str]; // fails on iPhone 5s
#else
        //[invocation setTarget:[NSNumber numberWithFloat:(float)i]]; // crashes in 'invoke' on iPhone 5s, works fine in simulator
        [invocation setTarget:[NSNumber numberWithInteger:i]]; // fails on iphone 5S
#endif
        [invocation setSelector:@selector(floatValue)];

        [invocation invoke];

        float f;
        [invocation getReturnValue:&f];

        NSLog(@"%f", f);
    }
}

0

根据@TomSwift的答案进行修复。

    - (void)testInvocation
    {
        NSInvocation *invocation = [[self class] invocationWithObject:self selector:@selector(getAFloat)];

        [invocation setTarget:self];
        [invocation invoke];

        double d;
        [invocation getReturnValue: &d];
        NSLog(@"d == %f", d);

        return YES;
    }

    + (NSInvocation *)invocationWithObject:(id)object selector:(SEL)selector
    {
        NSMethodSignature *sig = [object methodSignatureForSelector:selector];
        if (!sig) {
            return nil;
        }

    #ifdef __LP64__
        BOOL isReturnDouble = (strcmp([sig methodReturnType], "d") == 0);
        BOOL isReturnFloat = (strcmp([sig methodReturnType], "f") == 0);

        if (isReturnDouble || isReturnFloat) {
            typedef struct {double d;} doubleStruct;
            typedef struct {float f;} floatStruct;

            NSMutableString *types = [NSMutableString stringWithFormat:@"%s@:", isReturnDouble ? @encode(doubleStruct) : @encode(floatStruct)];
            for (int i = 2; i < sig.numberOfArguments; i++) {
                const char *argType = [sig getArgumentTypeAtIndex:i];
                [types appendFormat:@"%s", argType];
            }

            sig = [NSMethodSignature signatureWithObjCTypes:[types UTF8String]];
        }
    #endif

        NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
        [inv setSelector:selector];
        return inv;
    }

0

哇,今天这个问题似乎仍然无法解决,在模拟器中也是如此(我正在测试它)。我今天被这个 bug 搞了一下,发现我的双参数在调用选择器时总是返回零。

在我的情况下,很容易将其作为 NSNumber 发送,@(myDouble) 作为参数,然后在另一端用 myNumber.doubleValue 进行解包。

非常讨厌。


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