1. ホーム
  2. アイオス

[解決済み】Objective-CのMethod Swizzlingの危険性とは?

2022-04-04 15:05:52

質問

スウィズリングは危険な行為だという話を聞いたことがあります。スウィズリングという名前からしても、ちょっとしたチート行為であることがうかがえます。

メソッド・スウィズリング は、セレクタAを呼び出すと実際には実装Bが呼び出されるようにマッピングを変更することである。

スウィズリングを使うかどうかを決める人が、自分のやろうとしていることに対してそれだけの価値があるかどうか、十分な情報を得た上で判断できるように、リスクを形式化できないか。

  • ネーミングの衝突 : 後からクラスが機能を拡張して、あなたが追加したメソッド名を含むようになった場合、膨大なマナーの問題を引き起こすことになります。スウィズルなメソッド名を賢明につけることで、リスクを軽減しましょう。

解決方法は?

これは本当に素晴らしい質問だと思います。しかし、ほとんどの回答が本当の問題に取り組むのではなく、この問題を避けて、単にスウィズリングを使うなと言っているのは残念です。

メソッドシズリングの使用は、キッチンで鋭い包丁を使うようなものです。鋭利な包丁は大怪我をすると思って怖がっている人がいますが、実は 鋭い包丁の方が安全 .

メソッド・スウィズリングは、より良い、より効率的な、より保守的なコードを書くために使用することができます。また、悪用され、恐ろしいバグを引き起こすこともあります。

背景

すべてのデザインパターンに言えることですが、そのパターンがもたらす結果を十分に理解していれば、それを使うかどうか、より多くの情報を得た上で判断することができるようになります。シングルトンはかなり議論のあるものの良い例で、それには正当な理由があります - 適切に実装するのが本当に難しいのです。しかし、それでも多くの人がシングルトンを使うことを選択します。同じことが、スウィズリングについても言えます。良いことも悪いことも十分に理解した上で、自分なりの意見を持つべきでしょう。

ディスカッション

メソッドスウィズリングの落とし穴を紹介します。

  • メソッドスウィズリングはアトミックではありません
  • 未所有コードの挙動を変更する
  • ネーミングの衝突の可能性
  • スウィズリングでメソッドの引数が変更される
  • スウィズルの順番は重要
  • わかりにくい(再帰的に見える)
  • デバッグが困難

これらの指摘はすべて有効であり、それらに対処することで、メソッドスウィズリングに対する理解も、その結果を得るための手法も向上させることができるのです。1つずつ説明していきます。

メソッドスウィズリングはアトミックではない

同時に使用しても安全なメソッドスウィズリングの実装は、まだ見たことがありません。 1 . これは実は、メソッドスウィズリングを使いたいケースの95%では問題ないのです。通常は、単にメソッドの実装を置き換えるだけであり、その実装をプログラムの全期間にわたって使いたいだけです。つまり、メソッドのスウィズリングを行うのは +(void)load . その load クラスメソッドは、アプリケーションの開始時にシリアルに実行されます。ここでスウィズリングを行えば、並行処理の問題は発生しません。もし +(void)initialize しかし、スウィズリングの実装でレースコンディションが発生し、ランタイムが奇妙な状態になる可能性があります。

非所有コードの挙動を変更する

これはスウィズリングの問題ですが、要はそういうことです。目標は、そのコードを変更できるようにすることです。このことが大きな問題であると指摘される理由は、あなたが変更するのは、単に NSButton に対してではなく、すべての 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:)];

のメソッドを実行するとどうなるのでしょうか? NSButton がスウィズルされるのでしょうか?さて、ほとんどのスウィズリングは、それが setFrame: はすべてのビューで使用されるため 引上げる のインスタンスメソッドです。これは既存の実装を利用して setFrame: の中で NSButton クラスの実装を交換しても、すべてのビューに影響が出ないようにするためです。既存の実装は NSView . をスウィズすると、同じことが起こります。 NSControl (再び NSView の実装)。

を呼び出すと setFrame: を呼び出すと、スウィズしたメソッドが呼び出され、そのまま setFrame: メソッドに定義されている NSView . その NSControlNSView スウィズリングされた実装は呼び出されません。

しかし、その順番がこうだったらどうでしょう。

[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 上記で定義したスウィズリング方式で、トランポリンを使えば、スレッドセーフにすることができます。トランポリンは2つ必要です。メソッドの最初に、関数ポインタを代入する必要があります。 store のアドレスに到達するまで回転する関数に接続します。 store を指す点が変化しました。このようにすれば、スウィズルされたメソッドが store 関数ポインタを指定します。実装がクラスで定義されていない場合はトランポリンを使用し、トランポリンがスーパークラスのメソッドを適切に検索して呼び出すようにする必要があります。メソッドを定義して、スーパークラスの実装を動的に検索するようにすれば、スウィズリングの呼び出しの順番が問題になることはないでしょう。