Objective-C でメソッド呼び出しをインターセプトできますか? どのように?
編集: Mark Powellの回答により、部分的な解決策である-forwardInvocationメソッドが得られました。ただし、ドキュメントには、対応するメソッドがないオブジェクトがメッセージを送信された場合にのみ、 -forwardInvocation が呼び出されると記載されています。レシーバーにそのセレクターがある場合でも、すべての状況でメソッドが呼び出されるようにしたいと思います。
Objective-C でメソッド呼び出しをインターセプトできますか? どのように?
編集: Mark Powellの回答により、部分的な解決策である-forwardInvocationメソッドが得られました。ただし、ドキュメントには、対応するメソッドがないオブジェクトがメッセージを送信された場合にのみ、 -forwardInvocation が呼び出されると記載されています。レシーバーにそのセレクターがある場合でも、すべての状況でメソッドが呼び出されるようにしたいと思います。
メソッド呼び出しをスウィズルすることでそれを行います。すべてのリリースを NSTableView に取得するとします。
static IMP gOriginalRelease = nil;
static void newNSTableViewRelease(id self, SEL releaseSelector, ...) {
NSLog(@"Release called on an NSTableView");
gOriginalRelease(self, releaseSelector);
}
//Then somewhere do this:
gOriginalRelease = class_replaceMethod([NSTableView class], @selector(release), newNSTableViewRelease, "v@:");
詳細については、Objective C ランタイムドキュメントを参照してください。
objc_msgSend(receiver, selector, arguments)
Objective-C でのメッセージの送信は、関数またはそのバリアントobjc_msgSendSuper
の呼び出しobjc_msgSend_stret
に変換されますobjc_msgSendSuper_stret
。
これらの関数の実装を変更できれば、あらゆるメッセージを傍受できます。残念ながら、objc_msgSend
これは Objective-C ランタイムの一部であり、オーバーライドできません。
Google ブックスで論文を見つけました: A Reflective Architecture for Process Control Applications by Charlotte Pii Lunau。この論文では、オブジェクトのisa
クラス ポインタをカスタム MetaObject クラスのインスタンスにリダイレクトするというハックが紹介されています。したがって、変更されたオブジェクトを対象としたメッセージは、MetaObject インスタンスに送信されます。MetaObject クラスには独自のメソッドがないため、メッセージを変更されたオブジェクトに転送することで、転送呼び出しに応答できます。
この論文にはソースコードの興味深い部分は含まれておらず、そのようなアプローチが Cocoa に副作用をもたらすかどうかはわかりません。でもやってみるのも面白いかも。
アプリケーション コードから送信されたメッセージをログに記録する場合は、-forwardingTargetForSelector: ヒントが解決策の一部です。
オブジェクトをラップします。
@interface Interceptor : NSObject
@property (nonatomic, retain) id interceptedTarget;
@end
@implementation Interceptor
@synthesize interceptedTarget=_interceptedTarget;
- (void)dealloc {
[_interceptedTarget release];
[super dealloc];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"Intercepting %@", NSStringFromSelector(aSelector));
return self.interceptedTarget;
}
@end
次のようにします。
Interceptor *i = [[[Interceptor alloc] init] autorelease];
NSFetchedResultsController *controller = [self setupFetchedResultsController];
i.interceptedTarget = controller;
controller = (NSFetchedResultsController *)i;
メッセージ送信のログが表示されます。傍受されたオブジェクト内から送信された送信は、元のオブジェクトの「自己」ポインターを使用して送信されるため、傍受されないことに注意してください。
外部から呼び出されたメッセージのみをログに記録したい場合 (通常はデリゲートから呼び出され、どの種類のメッセージをいつ確認するかなど)、次のように RespondsToSelector をオーバーライドできます。
- (BOOL)respondsToSelector:(SEL)aSelector {
NSLog(@"respondsToSelector called for '%@'", NSStringFromSelector(aSelector));
// look up, if a method is implemented
if([[self class] instancesRespondToSelector:aSelector]) return YES;
return NO;
}
のサブクラスを作成してNSProxy
and を実装-forwardInvocation:
します-methodSignatureForSelector:
(または-forwardingTargetForSelector:
、自分でメソッドをいじるのではなく、単純に 2 番目のオブジェクトに指示する場合)。
NSProxy
に実装するために設計されたクラスです-forwardInvocation:
。いくつかの方法がありますが、ほとんどの場合、それらをキャッチしたくありません。たとえば、参照カウント メソッドをキャッチすると、ガベージ コレクション以外でプロキシの割り当てが解除されるのを防ぐことができます。ただし、NSProxy
絶対に転送する必要がある特定のメソッドがある場合は、そのメソッドを具体的にオーバーライドして-forwardInvocation:
手動で呼び出すことができます。NSProxy
メソッドがドキュメントの下にリストされているからといって、メソッドがそれを実装していることを意味するわけではなくNSProxy
、すべてのプロキシ オブジェクトがそれを持っていることが期待されているだけであることに注意してください。
これがうまくいかない場合は、状況に関する追加の詳細を提供してください。
NSObject
のメソッドが必要な場合があります-forwardInvocation
。これにより、メッセージをキャプチャし、ターゲットを変更してから再送信できます。
メソッド呼び出しを独自のものでスウィズルできます。これは、「インターセプト」でやりたいことを何でも実行し、元の実装を呼び出します。スウィズリングは class_replaceMethod() で行われます。
メソッド呼び出し、いいえ。メッセージの送信、はい。
メソッドが呼び出されたときに何かを行うには、イベント ベースのアプローチを試すことができます。そのため、メソッドが呼び出されると、リスナーによってピックアップされるイベントがブロードキャストされます。私はオブジェクティブ C は得意ではありませんが、Cocoa でNSNotificationCenterを使用して似たようなことを見つけました。
ただし、「インターセプト」が「停止」を意味する場合は、メソッドを呼び出す必要があるかどうかを判断するために、さらにロジックが必要になる可能性があります。