
本ブログは「The Right Way to Swizzle in Objective-C」の抄訳となります。
Swizzlingとは、メソッドの実装を別のものに置き換えることで、メソッドの機能を変更する行為のことで、通常は実行時に行われます。Swizzlingを使用したいと思う理由は様々で、イントロスペクション、デフォルトの動作のオーバーライド、あるいは動的なメソッドのロードなどがあります。Objective-CのSwizzlingについて議論している多くのブログ記事を見てきましたが、その多くはかなり悪い習慣を推奨しています。これらの悪しき習慣はスタンドアロンのアプロケーションを書いている場合には、実際には大きな問題ではありません。しかし、サードパーティの開発者のためにフレームワークを書いている場合、スウィズリングによって、すべてをスムーズに動かすための基本的な前提条件が崩れてしまいます。では、Objective-Cでの正しいSwizzlingの方法とは何でしょうか?
まずは基本的なことから説明します。Swizzlingとは、元のメソッドを自分のメソッドで置き換え、通常は置き換えたメソッドの中から元のメソッドを呼び出す行為を意味します。Objective-Cでは、Objective-Cランタイムで提供される関数を使用して、この行為を許可しています。ランタイムでは、Objective-CのメソッドはMethod
というC言語の構造体として表現され、次のように定義されたstruct objc_method
のtypedefとなります:
method_name
はメソッドのセレクタ、*method_types
はパラメータと戻り値の型エンコーディングのc文字列、method_imp
は実際の関数への関数ポインタです。(このIMP
については後で詳しく説明します)。
このオブジェクトにアクセスするには、以下のメソッドのいずれかを使用します(Objective-Cランタイムではさらに多くのオプションが用意されています)。
オブジェクトのMethod
構造体にアクセスすることで、それらの元の実装を変更することができます。method_imp
は IMP
型で、id (*IMP)(id, SEL, ...)
または、オブジェクトポインタ、セレクタ、及び項目の追加変数リストをパラメータとして受け取り、オブジェクトポインタを返す関数として定義される。これを変更するには、 IMP method_setImplementation(Method method, IMP imp)
を使用する。method_setImplementation()
に、変更したいMethod
構造体のmethod
とともに置き換える実装imp
を渡すと、Method
に関連するオリジナルのIMP
が返されます。これが正しいSwizzleの方法です。
正しくないSwizzleの方法は?
ここでは、Swizzleによく使われるパワーメソッドを紹介します。あるメソッドの実装を別のメソッドと交換するという一見簡単そうな方法ですが、明らかではない結果もあります。
この結果を理解するために、この関数が呼び出される前と後のm1とm2の構造を見てみましょう。
これらは、関数を呼び出す前のMethod
構造体です。これらの構造体を生成するObjective-Cのコードは、以下のようになります:
そして、呼び出します:
メソッドは次のようになります:
オリジナルのメソッド・コードを実行するには、-[self swizzle_originalMethodName]
を呼ばなければならないことに注意してください。しかしこれでは、オリジナルのメソッド・コードに渡される_cmd
の値が@selector(swizzle_originalMethodName)
になってしまい、もしメソッド・コードが_cmd
をメソッドのオリジナルの名前(originalMethodName
)に依存している場合には、このような結果になってしまいます。このようなSwizzlingの方法(下記の例)は、プログラムの正常な機能を妨げているので、避けるべきです。
では、正しいSwizzlingの方法を見てみましょう。method_setImplementation()
関数を使います。
Swizzleの正しいやり方
Objective-Cの関数 -[(void) swizzle_originalMethodName]
を作成する代わりに、IMP
の定義(より具体的にはswizzleするメソッドのシグネチャ)に準拠したCの関数を作成します†。
この関数を IMP
としてキャストすることができます:
そして、これをmethod_setImplementation()
に渡すことができます:
そして、 method_setImplementation()
は、オリジナルの IMP
を返します:
これで originalImp
を使ってオリジナルのメソッドを 呼び出す††ことができます:
ここでは、それをまとめた例をご紹介します:
以下のテストを行うことで確認できます:
結論として、他のサードパーティのSDKとの衝突を避けるためには、Objective-Cのメソッドとmethod_swapImplementations()
を使ってSwizzleするのではなく、Cの関数とmethod_setImplementation()
を使い、これらのCの関数をIMPとしてキャストしてください。これにより、新しいセレクタ名のような、Objective-Cメソッドに付随する余分な情報の荷物をすべて回避できます。 Swizzleをしたいのであれば、痕跡を残さないことが最良の結果です。
†Objective-Cのすべてのメソッドは、2つの隠れたパラメータ、すなわちselfへの参照(id self
)とメソッドのセレクタ(SEL _cmd
)を渡すことを忘れないでください。
††voidを返す場合、IMP
の呼び出しをケースに入れないといけないかもしれません。ARM
がすべてのIMP
がidを返すと仮定し、voidとprimitiveタイプを保持しようとするからです。
*Sign image courtesy of Shutterstock
The views expressed on this blog are those of the author and do not necessarily reflect the views of New Relic. Any solutions offered by the author are environment-specific and not part of the commercial solutions or support offered by New Relic. Please join us exclusively at the Explorers Hub (discuss.newrelic.com) for questions and support related to this blog post. This blog may contain links to content on third-party sites. By providing such links, New Relic does not adopt, guarantee, approve or endorse the information, views or products available on such sites.