ページ

2009年10月17日土曜日

Safari用独自プラグインを作る(4) - Method Swizzling を試す

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク


(前回)Cocoaの日々: Objective-C 2.0 ランタイムの情報

2.0 ランタイムから追加された method_exchangeImplementations を使ってメソッドを置換してみる。

具体的な方法は下記のサイトが参考になった。
MethodReplacement
CocoaDev: MethodSwizzling

Mac Dev Center のコードがシンプルでとてもわかりやすい。

が、ここでは Cocoa Dev のたくさん方法が書いてある中で下記のコードを使ってみた。
void Swizzle(Class c, SEL orig, SEL new)
{
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
 method_exchangeImplementations(origMethod, newMethod);
}

このコードでは最初にメソッドの追加を試みてメソッドを置き換えている。カテゴリを使う場合は常に exchangeImplementations が使われる。

実際にこれを試してみよう。


以前と同様に -[NSWindow sendEvent:] を置き換えてみる。

オリジナル: sendEvent:
新メソッド: _xc_sendEvent:

今回はサブクラスでは無くカテゴリを使ってみた。

 準備として、まず NSWindowのカテゴリを定義する。
@interface NSWindow (XCCateogry)
- (void)_xc_sendEvent:(NSEvent *)theEvent;
+ (void)_swizzleMethods;
@end

@implementation NSWindow (XCCategory)
- (void)_xc_sendEvent:(NSEvent *)theEvent;
{
 if ([theEvent type] == NSRightMouseDown) {
  NSLog(@"[1] NSRightMouseDown");
 }
 [self _xc_sendEvent:theEvent];
}
+ (void)_swizzleMethods
{
 Swizzle([NSWindow class], @selector(sendEvent:), @selector(_xc_sendEvent:));
}
@end

ポイントはオリジナルメソッドの呼び出しで自身のメソッドを呼ぶところ。
[self _xc_sendEvent:theEvent];

このメソッドが実行されている時は既にメソッドの交換が行われているので、この名前で呼び出すとオリジナルが呼ばれる。なお [seld sendMethod:theEvent] とすると Safariがクラッシュする(無限ループすることになる)。これはメッセージ sendMethod:を送ると、呼び出されるのは自分自身(_xc_sendEvent:)になるため。メソッドの交換により元の実装は _xc_sendEvent: というセレクタに紐付けられる。

イメージはこんな感じ。まず交換前の状態。



メッセージが送られるとセレクタ名でメソッドが検索され、対応する実装が呼び出される。

method_exchangeImplementations() によってこうなる。

この状態でメッセージ sendEvent: を送ると新規メソッドの実装が呼び出される。

最後に置き換え実行メソッド +[_swizzleMethods]を、エントリポイントから呼び出す。
static BOOL initialized_flag = NO;
+(void)load
{
 if (!initialized_flag) {
  NSLog(@"+[PluginController load] was called");
  [NSWindow _swizzleMethods];
  initialized_flag = YES;
 }
}


実行してみよう。

Safariのウィンドウ上で右クリックすると -[_xc_sendEvent:] が実行されているのが確認できた。また元々の動作(コンテキストメニューが表示される)も正常に働いている。

※ +[load] が2回、-[_xcsendEvent:] も1クリックあたり2回呼び出されているのが気になる。


(関連)
Cocoaの日々: Safari用独自プラグインを作る(1) - ひな形プロジェクト作成
Cocoaの日々: Safari用独自プラグインを作る(2) - Posing と Method Swizzling
Cocoaの日々: Safari用独自プラグインを作る(3) - Posing を試す



-------------
(以下、参考情報)
「SIMBLで Cocoaアプリをパワーアップ 」(PDF)
http://kirika.la.coocan.jp/archive/cocoastudy/200804/simbl-cocoa200804.pdf

【コラム】ダイナミックObjective-C (18) メソッドとは何か(1) - メソッド、セレクタ、メソッドの実装 | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (19) メソッドとは何か(2) - メソッドを取得する | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (20) メソッドとは何か(3) - メソッドの型を読み解く | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (21) メソッドとは何か(4) - セレクタの実体 | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (22) メソッドとは何か(5) - メソッドの実装 | エンタープライズ | マイコミジャーナル
そう、Objective-Cで自分に対してメッセージを送るときに利用される、あのselfだ。selfは、Objective-Cのキーワードではなく、変数なのだ。
そうだったのか。目から鱗。

【コラム】ダイナミックObjective-C (23) メッセージ送信(1) - objc_msgSendの実装 | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (24) メッセージ送信(2) - メソッドリストからメソッドを検索する | エンタープライズ | マイコミジャーナル