これは本当に素晴らしい質問だと思います。実際の質問に取り組むのではなく、ほとんどの回答が問題を回避し、単にスウィズリングを使用しないと述べているのは残念です.
メソッド シズリングを使用することは、キッチンで鋭いナイフを使用するようなものです。よく切れる包丁を怖がる方もいますが、切れ味の悪い包丁の方が安全です。
メソッド スウィズリングを使用すると、より優れた、より効率的で保守しやすいコードを記述できます。また、悪用されて恐ろしいバグにつながる可能性もあります。
バックグラウンド
すべての設計パターンと同様に、パターンの結果を十分に認識していれば、それを使用するかどうかについて、より多くの情報に基づいた決定を下すことができます。シングルトンはかなり物議をかもしているものの良い例であり、それには正当な理由があります — シングルトンを適切に実装するのは非常に困難です。ただし、多くの人はまだシングルトンを使用することを選択しています。スウィズリングについても同じことが言えます。良い面と悪い面の両方を完全に理解したら、自分の意見を形成する必要があります。
討論
メソッド スウィズリングの落とし穴のいくつかを次に示します。
- メソッドの入れ替えはアトミックではない
- 所有されていないコードの動作を変更します
- 命名の競合の可能性
- スウィズリングはメソッドの引数を変更します
- スウィズルの順序が重要
- わかりにくい(再帰的に見える)
- デバッグが難しい
これらのポイントはすべて有効であり、それらに対処することで、メソッドの入れ替えと結果を達成するために使用される方法論の両方の理解を深めることができます. 一つ一つ取っていきます。
メソッドの入れ替えはアトミックではない
同時に使用しても安全なメソッド スウィズリングの実装はまだ見たことがありません1。これは実際には、メソッド スウィズリングを使用する 95% のケースでは問題になりません。通常、メソッドの実装を単に置き換えたいだけであり、その実装をプログラムの存続期間全体にわたって使用したいと考えています。これは、 でメソッドのスウィズリングを行う必要があることを意味します+(void)load
。load
クラス メソッドは、アプリケーションの開始時に順次実行されます。ここでスウィズルを実行しても、同時実行性に問題はありません。ただし、をスウィズルする+(void)initialize
と、スウィズリングの実装で競合状態が発生し、ランタイムが奇妙な状態になる可能性があります。
所有されていないコードの動作を変更します
これはスウィズリングの問題ですが、要点のようなものです。目標は、そのコードを変更できるようにすることです。人々がこれを大したことだと指摘する理由は、NSButton
変更したい の 1 つのインスタンスだけでなくNSButton
、アプリケーション内のすべてのインスタンスに対して変更を行っているからです。このため、スウィズルをするときは注意が必要ですが、完全に避ける必要はありません。
このように考えてください...クラスのメソッドをオーバーライドし、スーパークラスのメソッドを呼び出さないと、問題が発生する可能性があります。ほとんどの場合、スーパークラスはそのメソッドが呼び出されることを期待しています (別の方法で文書化されていない限り)。これと同じ考えをスウィズリングに適用すると、ほとんどの問題をカバーできます。常に元の実装を呼び出します。そうしないと、変更しすぎて安全でなくなる可能性があります。
命名の競合の可能性
名前の競合は、Cocoa 全体で問題になっています。カテゴリのクラス名とメソッド名にプレフィックスを付けることがよくあります。残念ながら、名前の競合は私たちの言語の疫病です。ただし、スウィズリングの場合は、そうである必要はありません。メソッドのスウィズリングについての考え方を少し変える必要があるだけです。ほとんどのスウィズリングは次のように行われます。
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end
@implementation NSView (MyViewAdditions)
- (void)my_setFrame:(NSRect)frame {
// do custom work
[self my_setFrame:frame];
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}
@end
my_setFrame:
これは問題なく動作しますが、 が別の場所で定義されている場合はどうなるでしょうか? この問題はスウィズルに固有のものではありませんが、とにかく回避できます。この回避策には、他の落とし穴にも対処できるという追加の利点があります。代わりに、次のことを行います。
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
これは Objective-C とは少し似ていませんが (関数ポインターを使用しているため)、名前の競合が回避されます。原則として、標準のスウィズルとまったく同じことをしています。これは、以前からスウィズリングが定義されていたので、スウィズリングを使用してきた人にとっては少し変更になるかもしれませんが、最終的には、より優れていると思います。スウィズリング メソッドは次のように定義されます。
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
メソッドの名前を変更してスウィズリングすると、メソッドの引数が変更されます
これは私の心の中で大きなものです。これが、標準メソッドのスウィズルを実行すべきではない理由です。元のメソッドの実装に渡される引数を変更しています。これはそれが起こる場所です:
[self my_setFrame:frame];
この行の機能は次のとおりです。
objc_msgSend(self, @selector(my_setFrame:), frame);
ランタイムを使用して の実装を検索しますmy_setFrame:
。実装が見つかると、指定されたのと同じ引数で実装を呼び出します。見つかった実装は の元の実装でsetFrame:
あるため、先に進んでそれを呼び出しますが、_cmd
引数はそうsetFrame:
あるべきではありません。今my_setFrame:
です。元の実装が、受け取るとは予想していなかった引数で呼び出されています。これはダメです。
簡単な解決策があります — 上で定義した別のスウィズル手法を使用します。引数は変更されません。
スウィズルの順序が重要
メソッドが入れ替わる順序が重要です。setFrame:
が でのみ定義されていると仮定してNSView
、次の順序を想像してください。
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
メソッド onNSButton
がスウィズルされるとどうなりますか? ほとんどのスウィズルはsetFrame:
、すべてのビューの実装を置き換えないことを保証するため、インスタンス メソッドをプルアップします。setFrame:
これにより、既存の実装を使用してクラスで再定義されるNSButton
ため、実装の交換がすべてのビューに影響することはありません。既存の実装は で定義されたものNSView
です。NSControl
(再びNSView
実装を使用して)スウィズルをオンにすると、同じことが起こります。
したがって、ボタンを呼び出すsetFrame:
と、swizzled メソッドが呼び出され、setFrame:
最初に で定義されたメソッドに直接ジャンプしNSView
ます。NSControl
と入れ替えられたNSView
実装は呼び出されません。
しかし、注文が次の場合はどうでしょう。
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
ビューのスウィズリングが最初に行われるため、コントロールのスウィズリングは正しいメソッドを引き出すことができます。同様に、コントロールのスウィズリングはボタンのスウィズリングの前にあったため、ボタンはコントロールのスウィズリングされた の実装をプルアップsetFrame:
します。ちょっとややこしいですが、これが正しい順番です。この順序をどのように確保できますか?
繰り返しますが、load
物事をスウィズルするために使用してください。スウィズルしてload
、ロードされているクラスにのみ変更を加えれば、安全です。このload
メソッドは、サブクラスの前にスーパークラスの load メソッドが呼び出されることを保証します。私たちは正確な正しい順序を取得します!
わかりにくい(再帰的に見える)
伝統的に定義されたスウィズル方式を見ると、何が起こっているのかを判断するのは非常に難しいと思います。しかし、上でスウィズリングを行った別の方法を見ると、非常に簡単に理解できます。これはもう解決済みです!
デバッグが難しい
デバッグ中の混乱の 1 つは、入れ替わった名前が混同され、頭の中ですべてがごちゃごちゃになる奇妙なバックトレースを目にすることです。繰り返しますが、代替実装はこれに対処します。バックトレースで明確に名前が付けられた関数が表示されます。それでも、スウィズリングがどのような影響を与えているかを思い出すのが難しいため、スウィズリングはデバッグが難しい場合があります。コードをよく文書化してください (コードを目にするのは自分だけだと思っていても)。良い習慣に従えば、大丈夫です。マルチスレッド コードよりもデバッグが難しくありません。
結論
メソッドの入れ替えは、適切に使用すれば安全です。あなたが取ることができる簡単な安全対策は、スウィズルインだけload
です. プログラミングの多くのことと同様に、危険な場合もありますが、その結果を理解することで適切に使用できるようになります。
1上で定義したスウィズリング メソッドを使用すると、トランポリンを使用する場合にスレッド セーフにすることができます。トランポリンは2つ必要です。メソッドの開始時に、関数ポインター を、指すアドレスが変更さstore
れるまでスピンする関数に割り当てる必要があります。これにより、関数ポインターstore
を設定できるようになる前に、swizzled メソッドが呼び出されるという競合状態が回避されます。store
実装がクラスでまだ定義されていない場合はトランポリンを使用し、トランポリンを検索してスーパークラスメソッドを適切に呼び出す必要があります。スーパー実装を動的に検索するようにメソッドを定義すると、スウィズリング呼び出しの順序が問題にならないことが保証されます。