web-dev-qa-db-ja.com

Objective-Cでのメソッドスウィズルの危険性は何ですか?

メソッドのスウィズリングは危険な行為であると人々が述べているのを聞いたことがあります。スウィズルという名前でさえ、それがちょっとした詐欺であることを示唆しています。

Method Swizzling は、セレクタAの呼び出しが実際に実装Bを呼び出すようにマッピングを変更しています。これの1つの用途は、クローズドソースクラスの動作を拡張することです。

スウィズリングを使用するかどうかを決定している人が、やろうとしていることに価値があるかどうかについて十分な情報に基づいた決定を下せるように、リスクを形式化できますか。

例えば。

  • Naming Collisions:クラスが後で追加したメソッド名を含むように機能を拡張すると、巨大な問題が発生します。スウィズルされたメソッドに適切な名前を付けることにより、リスクを軽減します。
229
Robert

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

焼けるような方法を使用すると、キッチンで鋭いナイフを使用するようなものです。一部の人々は、自分自身をひどく切ると思うので鋭いナイフが怖いですが、真実は 鋭いナイフの方が安全です です。

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

バックグラウンド

すべての設計パターンと同様に、パターンの結果を十分に認識している場合、それを使用するかどうかについて、十分な情報に基づいた決定を下すことができます。シングルトンはかなり物議を醸すものの良い例であり、正当な理由のために-それらは適切に実装するのは本当に難しいです。しかし、多くの人々はまだシングルトンを使用することを選択しています。スウィズリングについても同じことが言えます。良い面と悪い面の両方を完全に理解したら、自分の意見を形成する必要があります。

討論

メソッドのスウィズリングの落とし穴のいくつかを以下に示します。

  • メソッドのスウィズリングはアトミックではありません
  • 非所有コードの動作を変更します
  • 可能な名前の競合
  • スウィズリングはメソッドの引数を変更します
  • スウィズルの順序が重要
  • 理解しにくい(再帰的に見える)
  • デバッグが難しい

これらのポイントはすべて有効であり、それらに対処することで、メソッドスウィズリングの理解と結果を達成するために使用される方法論の両方を改善できます。一度に一つずつ取ります。

メソッドのスウィズリングはアトミックではありません

同時に使用しても安全なメソッドスウィズルの実装はまだ見ていません。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:)];

NSButtonのメソッドがスウィズルされるとどうなりますか?ほとんどのスウィズルは、すべてのビューのsetFrame:の実装を置き換えないことを保証するため、インスタンスメソッドをpull upします。これは既存の実装を使用してNSButtonクラスのsetFrame:を再定義し、実装の交換がすべてのビューに影響を与えないようにします。既存の実装は、NSViewで定義された実装です。 NSControl(再びNSView実装を使用)でスウィズルするときにも同じことが起こります。

したがって、ボタンでsetFrame:を呼び出すと、スウィズルされたメソッドが呼び出され、NSViewで最初に定義されたsetFrame:メソッドに直接ジャンプします。 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を割り当てる必要があります。これにより、store関数ポインターを設定する前に、スウィズルされたメソッドが呼び出される競合状態が回避されます。その後、実装がクラスでまだ定義されていない場合にトランポリンを使用し、トランポリンを検索して、スーパークラスメソッドを適切に呼び出す必要があります。スーパー実装を動的に検索するようにメソッドを定義すると、スウィズル呼び出しの順序が問題にならないことが保証されます。

426
wbyoung

最初に、メソッドのスウィズリングが意味することを正確に定義します。

  • 最初にメソッド(Aと呼ばれる)に送信されたすべての呼び出しを新しいメソッド(Bと呼ばれる)に再ルーティングします。
  • メソッドBを所有している
  • メソッドAを所有してはいけません
  • メソッドBはいくつかの作業を行い、メソッドAを呼び出します。

メソッドのスウィズリングはこれよりも一般的ですが、これは私が興味を持っているケースです。

危険:

  • 元のクラスの変更。私たちはスウィズルしているクラスを所有していません。クラスが変更されると、スウィズルが機能しなくなる場合があります。

  • 保守が難しい。スウィズルされたメソッドを記述して維持する必要があるだけではありません。スウィズルを実行するコードを記述して維持する必要があります

  • デバッグが難しい。スウィズルの流れを追うのは難しく、スウィズルが実行されたことに気付かない人もいます。スウィズルからバグが導入された場合(おそらく元のクラスの変更による)、それらを解決するのは困難です。

要約すると、スウィズルを最小限に抑え、元のクラスの変更がスウィズルに与える影響を考慮する必要があります。また、何をしているかを明確にコメントして文書化する必要があります(または完全に回避する必要があります)。

11
Robert

本当に危険なのは、スウィズル自体ではありません。問題は、あなたが言うように、フレームワーククラスの動作を変更するためによく使用されることです。これらのプライベートクラスがどのように機能するかについて「危険な」ことを知っていることを前提としています。今日の変更が機能していても、Appleが将来クラスを変更し、変更が中断される可能性が常にあります。また、多くの異なるアプリがそれを行うと、Appleが既存のソフトウェアの多くを壊さずにフレームワークを変更するのが非常に難しくなります。

7
Caleb

この手法を使用しましたが、次のことを指摘したいと思います。

  • 文書化されていないが、望まれる副作用を引き起こす可能性があるため、コードを難読化します。コードを読むとき、コードベースを検索してスウィズルされているかどうかを覚えていない限り、必要な副作用の振る舞いを知らないかもしれません。コードが副作用のスウィズル動作に依存しているすべての場所を文書化することは常に可能ではないため、この問題を軽減する方法がわかりません。
  • 他の場所で使用したいスウィズル動作に依存するコードのセグメントを見つけた人は、スウィズルされたメソッドを見つけてコピーすることなく、単に他のコードベースに切り貼りすることができないため、コードの再利用性が低下する可能性があります。
5
user3288724

慎重に賢明に使用すると、エレガントなコードにつながる可能性がありますが、通常は混乱を招くコードになります。

特定の設計作業にとって非常にエレガントな機会を提供することを偶然知っている場合を除き、禁止すべきであると言いますが、なぜそれが状況にうまく当てはまるのか、そして代替が状況に対してエレガントに機能しない理由を明確に知る必要があります。

たとえば、メソッドスウィズリングの良いアプリケーションの1つは、スウィズリングです。これは、ObjCがKey Value Observingを実装する方法です。

悪い例は、クラスを拡張する手段としてメソッドスウィズリングに依存している可能性があり、これは非常に高い結合につながります。

5
Arafangion

最大の危険は、多くの望ましくない副作用を完全に偶然作成することだと感じています。これらの副作用は、自分自身を「バグ」として提示する可能性があり、その結果、間違った道をたどって解決策を見つけることになります。私の経験では、危険は判読できず、混乱し、イライラするコードです。誰かがC++で関数ポインターを使いすぎるようなものです。

4
A.R.

次のような奇妙なコードになる可能性があります

- (void)addSubview:(UIView *)view atIndex:(NSInteger)index {
    //this looks like an infinite loop but we're swizzling so default will get called
    [self addSubview:view atIndex:index];

uIマジックに関連する実際の製品コードから。

4
quantumpotato

メソッドのスウィズリングはユニットテストで非常に役立ちます。

モックオブジェクトを記述し、実際のオブジェクトの代わりにそのモックオブジェクトを使用することができます。コードをクリーンな状態に保ち、ユニットテストの動作を予測できます。 CLLocationManagerを使用するコードをテストするとします。ユニットテストはstartUpdatingLocationをスウィズルし、デリゲートに所定の場所のセットを供給し、コードを変更する必要がないようにします。

3
user3288724