#12 Objective-C - Method Swizzling を Swizzling する

こんにちは、去年は Cocos2d-x の記事を書いたのに一度も Cocos2d-x を使ったことがない @Gemmbu です。

今年は久しぶりに Objective-c でごりごり開発していました。 その中から C で出来るちょっと面白いネタを。

Method Swizzling とは?

簡単に説明すると、既に定義されているメソッドの実装を入れ替える機能です。

例えば以下のような Foo クラスを定義します。

// Foo.h
@interface Foo
- (void)func;
@end

// Foo.m
@implementation Foo
- (void)func
{
    NSLog(@"call func");
}
@end

以下のように使います。

Foo *foo = [Foo.alloc init];
[foo func];    // -> call func が出力される

これから [foo func] を Method Swizzling を行いましょう。 Objective-C にはカテゴリと呼ばれる機能があり、既存のクラスに新たにメソッドの追加が行えます。 それを用いて入れ替える先の実装を定義します。

// Foo+Swizzling.h
@interface Foo(Swizzling)
- (void)swizzling_func;
@end

// Foo+Swizzling.m
@implementation Foo
- (void)swizzling_func
{
    NSLog(@"call swizzling_func");
}
@end

これで動作させると以下のようになります。

Foo *foo = [Foo.alloc init];
[foo func];              // -> call func が出力される
[foo swizzling_func];    // -> call swizzling_func が出力される

準備が整いました、メソッドの実装を入れ替えます。

Method func           = class_getInstanceMethod([Foo class], @selector(func));
Method swizzling_func = class_getInstanceMethod([Foo class], @selector(swizzling_func));
method_exchangeImplementations(func, swizzling_func);

Foo *foo = [Foo.alloc init];
[foo func];              // -> call swizzling_func が出力される
[foo swizzling_func];    // -> call foo が出力される

メソッドの実装を入れ替えることができました。 Method Swizzling は非常に強力で Objective-C の全てのクラスに対して使用できます。 つまり自分で作成したクラス以外に行うことができるということです。 例えば、システムフレームワークに含まれるクラスであったり 外部ライブラリに含まれるクラスに対してでも行うことが可能です。

Method Swizzing を Swizzling する

非常に強力な Method Swizzling ですが、そのために外部ライブラリで行われていた場合には 気付かないうちに Method Swizzing されている場合があります。

もし、その実装にバグがあった場合には呼び出していることを意識してないため、 非常にデバッグしづらい状態になってしまいます。 そこで Method Swizzing に必要な class_getInstanceMethodmethod_exchangeImplementations を 逆に乗っ取ることにより Method Swizzling が行われていることを検知しましょう。

Method Swizzling は Objective-C のメソッドに対して行っていたのですが、 class_getInstanceMethod, method_exchangeImplementations は C 関数のため手法が異なります。

具体的には以下のコードを適当なソースコードに追加するだけです。

#import <objc/runtime.h>
#define _GNU_SOURCE
#include <dlfcn.h>

static Method (*original_class_getInstanceMethod)(Class aClass, SEL aSelector);
Method class_getInstanceMethod(Class aClass, SEL aSelector)
{
    NSLog(@"class_getInstanceMethod(%@, %s)", NSStringFromClass(aClass), sel_getName(aSelector));
    if(original_class_getInstanceMethod == NULL){
        original_class_getInstanceMethod
        = (Method(*)(Class, SEL)) dlsym(RTLD_NEXT, "class_getInstanceMethod");
    }
    return original_class_getInstanceMethod(aClass, aSelector);
}

static void (*original_method_exchangeImplementations)(Method m1, Method m2);
void method_exchangeImplementations(Method m1, Method m2)
{
    NSLog(@"exchangeImplementations(%s, %s)", sel_getName(method_getName(m1)),
                                              sel_getName(method_getName(m2)));
    if(original_method_exchangeImplementations == NULL){
        original_method_exchangeImplementations
        = (void(*)(Method, Method)) dlsym(RTLD_NEXT, "method_exchangeImplementations");
    }
    original_method_exchangeImplementations(m1, m2);
}

上記のコードを含めることにより class_getInstanceMethod, method_exchangeImplementations の実行時にログ出力され、どこで Method Swizzling が行われているのか確認できます。


カヤックでは Objective-C に限らず C 言語の知識が豊富な iOS で開発できるエンジニアを募集しています!

明日は @DevMassive さんです。お楽しみに!