1

对于我的游戏的在线模式,我使用 的context属性GKScore,并且由于所有支持 Game Center 的设备都可以更新到 iOS 5(context添加属性时),我要求该context属性可以在线玩。但是,我在实现此运行时检查时遇到问题。我假设我可以[GKScore instancesRespondToSelector:@selector(setContext:)]用来检查它的存在,但这在 iOS 5 和 5.1 模拟器以及 for @selector(context). 为什么会发生这种情况,请问执行此检查的最干净和正确的方法是什么?

4

4 回答 4

4

这看起来像是 GK 实现中的一个错误。

考虑以下代码...

// Get the C-functions that are really called when the selector message is sent...
typedef BOOL (*XX)(id, SEL, SEL);
XX classImpNSObject = (XX)[NSObject
    methodForSelector:@selector(instancesRespondToSelector:)];
XX classImpGKScore = (XX)[GKScore
    methodForSelector:@selector(instancesRespondToSelector:)];
XX instImpNSObject = (XX)[NSObject
    instanceMethodForSelector:@selector(respondsToSelector:)];
XX instImpGKScore = (XX)[GKScore
    instanceMethodForSelector:@selector(respondsToSelector:)];

// See that the same C function is called for both of these...
NSLog(@"instancesRespondToSelector: %p, %p", classImpNSObject, classImpGKScore);

// But, different functions are called for these...
NSLog(@"respondsToSelector: %p, %p", instImpNSObject, instImpGKScore);

// Invoke to C-Functions for instancesRespondToSelector:
NSLog(@"NSObject instancesRespondToSelector: context: %s",
    classImpNSObject(
        [NSObject class],
        @selector(instancesRespondToSelector:),
        @selector(context))
    ? "YES" : "NO");
NSLog(@"GKScore instancesRespondToSelector: context: %s",
    classImpGKScore(
        [GKScore class],
        @selector(instancesRespondToSelector:),
        @selector(context))
    ? "YES" : "NO");

// Invoke the C functions for respondsToSelector:
GKScore *gkScore = [[GKScore alloc] init];
NSLog(@"NSObject respondsToSelector: context: %s",
    instImpNSObject(
        gkScore,
        @selector(respondsToSelector:),
        @selector(context))
    ? "YES" : "NO");
NSLog(@"GKScore respondsToSelector: context: %s",
    instImpGKScore(
        gkScore,
        @selector(respondsToSelector:),
        @selector(context))
    ? "YES" : "NO");

基本上,我们只是提取了响应这些消息时调用的 C 函数。

如您所见,NSObject 和 GKScore 使用完全相同的 C 函数实现instancesRespondToSelector:。但是,它们对respondsToSelector:. 这意味着用自己的实现GKScore覆盖respondsToSelector:(但不覆盖instancesRespondToSelector.

如果将相同的GKScore实例发送到不同的 C 实现,则respondsToSelector:某些选择器会得到不同的结果(显然,或者没有理由提供子类实现)。

看起来他们为一些特殊属性做了一些时髦的事情,并提供了一个覆盖respondsToSelector:来处理特殊情况,但忘记了确保 instancesRespondToSelector:做正确的事情。

如果您想浏览汇编代码,请设置断点,我相信您可以看到差异。

我没有那样做。

我个人的好奇心只会带我走这么远:-)

对于您的情况,尝试检测代码中的方法实现,我建议创建一个临时GKScore对象来进行测试,缓存该结果并释放临时对象。

于 2012-09-02T12:49:21.813 回答
1

如果您专门寻找属性的存在,您应该使用 Objective-C 运行时函数:

class_getProperty(Class cls, const char *name)

要使用它,您必须导入:

#import <objc/runtime.h>

作为一个小测试示例,以下是测试特定属性是否存在的方法:

#import <objc/runtime.h>

//...

objc_property_t realP = class_getProperty([GKScore class], "context");
objc_property_t fakeP = class_getProperty([GKScore class], "fakeContext");


if (realP) {
    NSLog(@"context exists");
}
if (!fakeP) {
    NSLog(@"fakeContext does not exist");
}
// Both statements will log correctly.

至于为什么 GKScore 实例似乎没有响应正确的选择器,我的想法是可能会声明上下文属性,@dynamic因此 会返回(请参阅此问题)。不知道内部细节,这就是我所能建议的,但如果你只是想测试一个属性的存在,上面的示例代码就可以了。+instancesRespondToSelector:-respondsToSelector:NO

顺便说一句,如果您不希望在 Objective-C 运行时中包含浮动,您可能希望将此行为封装在一个类中或将其包装在选择器中,而不是一字不差地将其粘贴在某个地方。当然,这完全取决于你。

于 2012-08-31T00:24:05.467 回答
1

我无法完全解释这一点,但类的实例化对象GKScore将返回YESrepondsToSelector(context)即使类说它不会。如果没有其他解决方案有效,请构造一个GKScore对象来查询它。


我想知道是否[[GKScore alloc] init]实际上返回了一个类型不是GKScore. 这可能发生。

GKScore *instantiatedScore = [[GKScore alloc] init]; // Add autorelease if using manual reference counting.
NSString* className = NSStringFromClass([instantiatedScore class]);
NSLog(@"instantiatedScore class name = %@", className);

但是,根据这个输出,它没有:

instantiatedScore class name = GKScore

GKSCore.h我想知道头文件中的编译器指令是否会影响这一点。它定义了两个仅在 iOS 5.0 或更高版本中可用的属性:contextshouldSetDefaultLeaderboard. 也许那些编译器指令意味着该类不能保证它会支持这两个属性。

在这个假设下[GKScore instancesRepondToSelector:@selector(category)]应该返回YES,但[GKScore instancesRepondToSelector:@selector(shouldSetDefaultLeaderboard)]应该返回NO

GKScore *instantiatedScore = [[GKScore alloc] init]; // Add autorelease if using manual reference counting.
NSLog(@"GKScore category = %d", [GKScore instancesRespondToSelector:@selector(category)]);
NSLog(@"instantiatedScore category = %d", [instantiatedScore respondsToSelector:@selector(category)]);

NSLog(@"GKScore context = %d", [GKScore instancesRespondToSelector:@selector(context)]);
NSLog(@"instantiatedScore context = %d", [instantiatedScore respondsToSelector:@selector(context)]);

NSLog(@"GKScore shouldSetDefaultLeaderboard = %d", [GKScore instancesRespondToSelector:@selector(shouldSetDefaultLeaderboard)]);
NSLog(@"instantiatedScore shouldSetDefaultLeaderboard = %d", [instantiatedScore respondsToSelector:@selector(shouldSetDefaultLeaderboard)]);

但是,输出比这更奇怪:

GKScore category = 0
instantiatedScore category = 1
GKScore context = 0
instantiatedScore context = 1
GKScore shouldSetDefaultLeaderboard = 1
instantiatedScore shouldSetDefaultLeaderboard = 1
于 2012-08-29T05:37:57.723 回答
1

我也遇到过这个问题,但在我的情况下是GKTurnBasedMatchParticipant。我快速转储了将#instancesRespondToSelector:发送到此类的每个属性的结果。

结果如下:

1   playerID            false
2   lastTurnDate        false
3   status              true
4   matchOutcome        false
5   matchOutcomeString  true
6   isWinner            true
7   invitedBy           false
8   inviteMessage       false
9   internal            true

注意有多少属性报告它们不能作为选择器发送。但是,还要注意一个额外的“内部”属性。现在看看查询这个内部对象是否会响应属性选择器的结果:

1   playerID            true
2   lastTurnDate        true
3   status              true
4   matchOutcome        true
5   matchOutcomeString  false
6   isWinner            false
7   invitedBy           true
8   inviteMessage       true
9   internal            false

因此,许多缺失的属性都在这里。我想利用未记录的“内部”功能来解决明显的 Apple 错误并不是很安全,但知道它仍然很有趣。

编辑:经过一天的闲逛,我在这里发现了问题。这些流氓属性实际上被设置为转发方法以转发到“内部”对象。作为一个 ObjectiveC 菜鸟,我没有意识到这是一件完全可以接受的事情。

就我而言,我不只是试图检测对象是否响应选择器,而且我实际上也想调用它。因此,处理转发的一般解决方案是:

(a) 要检查响应的可用性,请使用 [instance #respondsToSelector: sel] 而不是 [[instance class] instanceRespondsToSelector: del]。

(b) 要调用可能会或可能不会被转发的方法,请执行以下操作:

NSMethodSignature *signature = [instance methodSignatureForSelector:sel];
if (!signature) {
    // It's possible this is a message forwarding selector, so try this before giving up.
    NSObject *fwd=[instance forwardingTargetForSelector:sel];
    if (fwd && (signature= [fwd methodSignatureForSelector:sel]))
        // Redirect to the forwarding target
        instance=fwd;
    else {
        // ERROR case - selector is really not supported
    }
}

NSInvocation *invocation=[NSInvocation invocationWithMethodSignature:signature];

// Proceed with invocation setup

我希望这有助于防止其他人像我一样浪费时间。

于 2012-09-22T20:07:16.293 回答