Objective-CのSwizzleの正しいやり方

公開済み 所要時間:約 12分
RightWay_FINAL

Swizzlingとは、メソッドの実装を別のものに置き換えることで、メソッドの機能を変更する行為のことで、通常は実行時に行われます。Swizzlingを使用したいと思う理由は様々で、イントロスペクション、デフォルトの動作のオーバーライド、あるいは動的なメソッドのロードなどがあります。Objective-CのSwizzlingについて議論している多くのブログ記事を見てきましたが、その多くはかなり悪い習慣を推奨しています。これらの悪しき習慣はスタンドアロンのアプロケーションを書いている場合には、実際には大きな問題ではありません。しかし、サードパーティの開発者のためにフレームワークを書いている場合、スウィズリングによって、すべてをスムーズに動かすための基本的な前提条件が崩れてしまいます。では、Objective-Cでの正しいSwizzlingの方法とは何でしょうか?

まずは基本的なことから説明します。Swizzlingとは、元のメソッドを自分のメソッドで置き換え、通常は置き換えたメソッドの中から元のメソッドを呼び出す行為を意味します。Objective-Cでは、Objective-Cランタイムで提供される関数を使用して、この行為を許可しています。ランタイムでは、Objective-CのメソッドはMethodというC言語の構造体として表現され、次のように定義されたstruct objc_methodのtypedefとなります:

struct objc_method
     SEL method_name         OBJC2_UNAVAILABLE;
     char *method_types      OBJC2_UNAVAILABLE;
     IMP method_imp          OBJC2_UNAVAILABLE;
}

method_nameはメソッドのセレクタ、*method_typesはパラメータと戻り値の型エンコーディングのc文字列、method_impは実際の関数への関数ポインタです。(このIMPについては後で詳しく説明します)。

このオブジェクトにアクセスするには、以下のメソッドのいずれかを使用します(Objective-Cランタイムではさらに多くのオプションが用意されています)。

Method class_getClassMethod(Class aClass, SEL aSelector);
Method class_getInstanceMethod(Class aClass, SEL aSelector);

オブジェクトのMethod構造体にアクセスすることで、それらの元の実装を変更することができます。method_impIMP 型で、id (*IMP)(id, SEL, ...)または、オブジェクトポインタ、セレクタ、及び項目の追加変数リストをパラメータとして受け取り、オブジェクトポインタを返す関数として定義される。これを変更するには、 IMP method_setImplementation(Method method, IMP imp)を使用する。method_setImplementation()に、変更したいMethod構造体のmethodとともに置き換える実装impを渡すと、Methodに関連するオリジナルのIMPが返されます。これが正しいSwizzleの方法です。

正しくないSwizzleの方法は?

ここでは、Swizzleによく使われるパワーメソッドを紹介します。あるメソッドの実装を別のメソッドと交換するという一見簡単そうな方法ですが、明らかではない結果もあります。

void method_exchangeImplementations(Method m1, Method m2)

この結果を理解するために、この関数が呼び出される前と後のm1とm2の構造を見てみましょう。

Method m1 { //this is the original method. we want to switch this one with
             //our replacement method
      SEL method_name = @selector(originalMethodName)
      char *method_types = “v@:“ //returns void, params id(self),selector(_cmd)
      IMP method_imp = 0x000FFFF (MyBundle`[MyClass originalMethodName])
 }
Method m2 { //this is the swizzle method. We want this method executed when [MyClass
             //originalMethodName] is called
       SEL method_name = @selector(swizzle_originalMethodName)
       char *method_types = “v@:”
       IMP method_imp = 0x1234AABA (MyBundle`[MyClass swizzle_originalMethodName])
 }

これらは、関数を呼び出す前のMethod構造体です。これらの構造体を生成するObjective-Cのコードは、以下のようになります:

@implementation MyClass
     - (void) originalMethodName //m1
     {
              //code
     }
     - (void) swizzle_originalMethodName //m2
     {
             //…code?
            [self swizzle_originalMethodName];//call original method
            //…code?
     }
 @end

そして、呼び出します:

m1 = class_getInstanceMethod([MyClass class], @selector(originalMethodName));
m2 = class_getInstanceMethod([MyClass class], @selector(swizzle_originalMethodName));
method_exchangeImplementations(m1, m2)

メソッドは次のようになります:

Method m1 { //this is the original Method struct. we want to switch this one with
             //our replacement method
     SEL method_name = @selector(originalMethodName)
     char *method_types = “v@:“ //returns void, params id(self),selector(_cmd)
     IMP method_imp = 0x1234AABA (MyBundle`[MyClass swizzle_originalMethodName])
 }
Method m2 { //this is the swizzle Method struct. We want this method executed when [MyClass
            //originalMethodName] is called
     SEL method_name = @selector(swizzle_originalMethodName)
     char *method_types = “v@:”
     IMP method_imp = 0x000FFFF (MyBundle`[MyClass originalMethodName])
 }

オリジナルのメソッド・コードを実行するには、-[self swizzle_originalMethodName]を呼ばなければならないことに注意してください。しかしこれでは、オリジナルのメソッド・コードに渡される_cmdの値が@selector(swizzle_originalMethodName)になってしまい、もしメソッド・コードが_cmdをメソッドのオリジナルの名前(originalMethodName)に依存している場合には、このような結果になってしまいます。このようなSwizzlingの方法(下記の例)は、プログラムの正常な機能を妨げているので、避けるべきです。

- (void) originalMethodName //m1
 {
          assert([NSStringFromSelector(_cmd) isEqualToString:@“originalMethodNamed”]); //this fails after swizzling //using
          //method_exchangedImplementations()
          //…
 }

では、正しいSwizzlingの方法を見てみましょう。method_setImplementation()関数を使います。

Swizzleの正しいやり方

Objective-Cの関数 -[(void) swizzle_originalMethodName] を作成する代わりに、IMPの定義(より具体的にはswizzleするメソッドのシグネチャ)に準拠したCの関数を作成します†。

void __Swizzle_OriginalMethodName(id self, SEL _cmd)
 {
      //code
 }

この関数を IMPとしてキャストすることができます:

IMP swizzleImp = (IMP)__Swizzle_OriginalMethodName;

そして、これをmethod_setImplementation()に渡すことができます:

method_setImplementation(method, swizzleImp);

そして、 method_setImplementation() は、オリジナルの IMPを返します:

IMP originalImp = method_setImplementation(method,swizzleImp);

これで originalImp を使ってオリジナルのメソッドを 呼び出す††ことができます:

originalImp(self,_cmd);††

ここでは、それをまとめた例をご紹介します:

@interface SwizzleExampleClass : NSObject
 - (void) swizzleExample;
 - (int) originalMethod;
 @end
static IMP __original_Method_Imp;
 int _replacement_Method(id self, SEL _cmd)
 {
      assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethod"]);
      //code
     int returnValue = ((int(*)(id,SEL))__original_Method_Imp)(self, _cmd);
    return returnValue + 1;
 }
 @implementation SwizzleExampleClass
- (void) swizzleExample //call me to swizzle
 {
     Method m = class_getInstanceMethod([self class],
 @selector(originalMethod));
     __original_Method_Imp = method_setImplementation(m,
 (IMP)_replacement_Method);
 }
- (int) originalMethod
 {
        //code
        assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethod"]);
        return 1;
 }
@end

以下のテストを行うことで確認できます:

SwizzleExampleClass* example = [[SwizzleExampleClass alloc] init];
int originalReturn = [example originalMethod];
[example swizzleExample];
int swizzledReturn = [example originalMethod];
assert(originalReturn == 1); //true
assert(swizzledReturn == 2); //true

結論として、他のサードパーティの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タイプを保持しようとするからです。

IMP anImp; //represents objective-c function
          // -UIViewController viewDidLoad;
 ((void(*)(id,SEL))anImp)(self,_cmd); //call with a cast to prevent
                                     // ARC from retaining void.

*Sign image courtesy of Shutterstock