对于我的游戏的在线模式,我使用 的context
属性GKScore
,并且由于所有支持 Game Center 的设备都可以更新到 iOS 5(context
添加属性时),我要求该context
属性可以在线玩。但是,我在实现此运行时检查时遇到问题。我假设我可以[GKScore instancesRespondToSelector:@selector(setContext:)]
用来检查它的存在,但这在 iOS 5 和 5.1 模拟器以及 for @selector(context)
. 为什么会发生这种情况,请问执行此检查的最干净和正确的方法是什么?
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
对象来进行测试,缓存该结果并释放临时对象。
如果您专门寻找属性的存在,您应该使用 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 运行时中包含浮动,您可能希望将此行为封装在一个类中或将其包装在选择器中,而不是一字不差地将其粘贴在某个地方。当然,这完全取决于你。
我无法完全解释这一点,但类的实例化对象GKScore
将返回YES,repondsToSelector(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 或更高版本中可用的属性:context
和shouldSetDefaultLeaderboard
. 也许那些编译器指令意味着该类不能保证它会支持这两个属性。
在这个假设下[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
我也遇到过这个问题,但在我的情况下是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
我希望这有助于防止其他人像我一样浪费时间。