私のゲームのオンラインモードでは、のcontext
プロパティを使用していGKScore
ます。GameCenterをサポートするすべてのデバイスがiOS 5(context
プロパティが追加されたとき)に更新できるため、context
プロパティをオンラインでプレイできるようにする必要があります。ただし、このランタイムチェックの実装に問題があります。私は[GKScore instancesRespondToSelector:@selector(setContext:)]
その存在を確認するために使用できると思っていましたが、これはiOS 5と5.1のシミュレーター、およびの場合にfalseを返します@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:
ます。ただし、は異なるC関数の実装を使用しrespondsToSelector:
ます。これは、独自の実装でGKScore
オーバーライドすることを意味します(ただし、はオーバーライドしません。respondsToSelector:
instancesRespondToSelector
GKScore
同じインスタンスを異なるC実装に送信するとrespondsToSelector:
、一部のセレクターで異なる結果が得られます(明らかに、またはサブクラス実装を提供する理由はありません)。
彼らはいくつかの特別なプロパティに対してファンキーなことをしrespondsToSelector:
、特別なケースを処理するためのオーバーライドを提供したようですが、正しいことを確実に行うことを忘れて instancesRespondToSelector:
いました。
アセンブリコードを調べたい場合は、ブレークポイントを設定してください。違いがわかると思います。
私はそれをしなかった。
私自身の個人的な好奇心は今のところ私を運ぶだけです:-)
コードでメソッドの実装を検出しようとする状況ではGKScore
、テストを実行するための一時オブジェクトを作成し、その結果をキャッシュして、一時オブジェクトを解放することをお勧めします。
私もこの問題に遭遇しましたが、私の場合は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 class] instanceRespondsToSelector:del]ではなく[instance #respondsToSelector:sel]を使用します。
(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
これが、他の人が私と同じくらい多くの時間を無駄にするのを防ぐのに役立つことを願っています。
これを完全に説明することはできませんが、クラスのインスタンス化されたオブジェクトは、クラスがそうではないと言っている場合でも、YESをGKScore
返します。他の解決策が機能しない場合は、クエリを実行するためだけにオブジェクトを作成します。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以降でのみ使用可能な2つのプロパティを定義します:context
とshouldSetDefaultLeaderboard
。たぶん、これらのコンパイラ指令は、クラスがこれら2つのプロパティをサポートすることを保証できないことを意味します。
この仮説では、 YES[GKScore instancesRepondToSelector:@selector(category)]
を返す必要がありますが、 NOを返す必要があります。[GKScore instancesRepondToSelector:@selector(shouldSetDefaultLeaderboard)]
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
プロパティの存在を具体的に探している場合は、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ランタイムにインクルードしたくない場合は、逐語的に貼り付けるのではなく、この動作をクラスにカプセル化するか、セレクターでラップすることをお勧めします。もちろん、それは完全にあなた次第です。