実装されているメソッドを別のメソッドに差し替える : Objective-C プログラミング
PROGRAM
実装されているメソッドを別のメソッドに差し替える
Objective-C ではランタイムの機能を使うことで、実行時に、実装済みのメソッドを別のメソッドで置き換えることができます。
今回は例えば、次のメソッドが実装されているとします。
@interface MyClass1 : NSObject
- (void)originalMethod;
- (void)replaceMethod;
この originalMethod を replaceMethod で置き換えてみます。
今回は以下で示すように、自分で実装したメソッドを置き換えるだけですけど、この方法を使うことで UILabel などの用意されているメソッドの機能も自由に置き換えることができます。
メソッドの機能を置き換える
まずは準備として、Objective-C ランタイム機能を使いたいソースコードに objc/runtime.h をインポートします。
#import <objc/runtime.h>
これで Objective-C ランタイムの機能を使えれるようになりました。
続いて、メソッドの置換を行います。
メソッドの置き換え処理はどこで書いてもいいようですが、メソッドを置き換えたいクラスがランタイムに読み込まれたときに呼ばれる +load メソッド内で書くのが、制御しやすいように思います。
+(void)load
{
// 置き換え対象のクラスとメソッドです。
Class targetClass = [MyClass1 class];
SEL targetSelector = @selector(originalMethod);
// 新しい機能が実装されているクラスを指定します。
Class sourceClass = [MyClass1 class];
// 新しい機能の、実装とタイプを取得しています。
Method newMethod = class_getInstanceMethod(sourceClass, @selector(replaceMethod));
IMP newImplementation = method_getImplementation(newMethod);
const char* newTypes = method_getTypeEncoding(newMethod);
// 元のメソッドの、実装とタイプを、新しいメソッドのもので差し替えます。
class_replaceMethod(targetClass, targetSelector, newImplementation, newTypes);
}
これが実行された瞬間、-originalMethod の実装が -replaceMethod の内容に置き換えられます。
今回の例では +load で実行したので、インスタンスを使い始めたその時から -originalMethod を呼び出すと -replaceMethod の結果が得られます。
ただし、これでメソッドが「交換」される訳ではないので、-replaceMethod を呼び出したときには -replaceMethod の結果がそのまま得られます。交換したい場合には、上記と平行して -replaceMethod の実装とタイプを取得して、それを -originalMethod と置き換えるようにします。
なお、この +load メソッドは、派生クラスを作った場合でも、基底クラスと派生クラスのそれぞれで呼ばれるようになっています。
そのため、派生クラスが +load メソッドを独自に実装した場合でも、メソッドの置換処理を漏れなく実行することができます。
派生クラスでの振る舞い
今回の例のように、対象とするクラスを MyClass1 のように決め打ちにした場合、置き換えるメソッドはそのクラスのものになります。
派生元 (super) 側のメソッドを置き換えた場合
例えば、MyClass1 から MyClass2 を派生していて、上記の例のようにメソッドを置き換えた場合は、実装は次のようになります。
クラス | originalMethod の実装 | replaceMethod の実装 |
---|---|---|
MyClass1 | replaceMethod | replaceMethod |
MyClass2 | originalMethod | replaceMethod |
そのため MyClass2 インスタンスの originalMethod を呼び出したときには、本来の実装通りの originalMethod が呼び出されます。
このとき、MyClass2 の originalMethod 内で [super originalMethod] を呼び出すと、置き換わった後の replaceMethod が呼び出されるようになっています。
派生先 (self) 側のメソッドを置き換えた場合
なお、メソッドを置き換えるときに、対象として [self class] を指定することもできます。
この場合、+load からであればまさに自分自身を取得することができるようですが、インスタンスメソッドで置き換えを実行する場合には、インスタンス化されたクラスのメソッドを置き換えることになります。
つまり、MyClass2 インスタンスで [self class] を実行した場合、それは [MyClass2 class] と同じことです。
どちらにせよ、そのようにして MyClass2 のメソッドを置き換えるようにプログラムされた場合、その指定通り、MyClass2 の -originalMethod が -replaceMethod に置き換わります。
クラス | originalMethod の実装 | replaceMethod の実装 |
---|---|---|
MyClass1 | originalMethod | replaceMethod |
MyClass2 | replaceMethod | replaceMethod |
これにより、MyClass2 インスタンスの -originalMethod を呼び出したときに、直ちに -replaceMethod が呼ばれるようになります。
もちろん、MyClass2 の -originalMethod が置き換えられた -replaceMethod 中では、基底クラスの -replaceMethod を呼び出すときには [super replaceMethod] とプログラムされているので、そのまま MyClass1 の -replaceMethod が実行されます。
逆に MyClass2 のインスタンスから [super originalMethod] を呼び出した場合には、MyClass1 の -originalMethod は置き換えられていないので、そのまま本来の実装通り MyClass1 の -originalMethod が呼び出されます。
派生元を派生先で置き換えることも
この機能を使用することで、派生先のメソッドで派生元のメソッドを置き換えることもできます。
例えば self.originalMethod → super.originalMethod という呼び出しのメソッドがあったとします。
この super.originalMethod を self.replaceMethod に置き換えると、その通りに置き換わります。このとき、self.replaceMethod → super.replaceMethod という流れだったとすると、最終的には次の呼び出し順序になります。
self.originalMethod >> self.replaceMethod >> super.replaceMethod
ちなみに、super.originalMethod を self.originalMethod で置き換えると、次のように無限ループに陥ります。
self.originalMethod >> self.originalMethod >> self.originalMethod >> ...............
このように、Objective-C ランタイムを使ったメソッドの置換はオブジェクト指向の整合性を根本から壊しかねないので、十分注意して使用する必要があります。
そもそも、余程高度なことをしない限りは使わない方が賢明なのかもしれません。
__FUNCTION__ で取得できる名称
このようにメソッドを置き換えた場合でも、プログラミング時に実装した本来のメソッド名が取得できました。
今回の例では、置換後の originalMethod を呼び出して、実際には replaceMethod の実装が呼び出されましたが、このときも __FUNCTION__ をログに出力してみると "-[MyClass1 replaceMethod]" という文字列が表示されました。
そんなことを試していたらさっそく、__FUNCTION__ はマクロだからコンパイルの段階(実行前)に決定されるということを @k_katsumi さんに教えて頂きました。
なるほど、プリプロセッサで文字列化されるのだから、その後のランタイムでメソッドを置き換えたところで何も影響されないですね。
[ もどる ]