本ブログは「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となります:
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_imp
は IMP
型で、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
本ブログに掲載されている見解は著者に所属するものであり、必ずしも New Relic 株式会社の公式見解であるわけではありません。また、本ブログには、外部サイトにアクセスするリンクが含まれる場合があります。それらリンク先の内容について、New Relic がいかなる保証も提供することはありません。