既存クラスのメソッドを入れ替える(Method Swizzling)

  • 114
    Like
  • 1
    Comment

状況:

  • 画面が10個ほどあるiPhoneアプリ
  • UIViewControllerのviewWillAppear等にログを仕込みたい。
  • すでにUIViewControllerを継承したクラスが多数あって、全部にログを仕込むのはめんどくさいしダサくていやだ。

解決策:

  • method_exchangeImplementationsでメソッドを入れ替えてしまえばいいよ!

黒魔法へようこそ

Objective-C Runtime には、method_exchangeImplementations()という関数が用意されています。これで「メソッドの交換」が可能となります。これはSDKで提供されているクラスのメソッドも入れ替える事が可能です。なのでUIViewControllerのviewWillAppear等を自作メソッドと差し替える事が出来ます。この差し替えは、動的に行われるので、動作する環境に応じて入れ替えをコントロールしたり、元に戻したりといった事が可能となります。

この方法は、"Method Swizzling"と呼ばれるものみたいです。

以下のような実装をします。

  • カテゴリ拡張で、入れ替えるメソッドを用意します。
    • このとき、「元々実装していたメソッド名」と「入れ替えた実装のメソッド名」も入れ替わるので、無限ループしそうなソースコードになります。
  • class_getInstanceMethod()で、メソッドへの参照(Method)を取得します。
  • method_exchangeImplementations()で、メソッドを入れ替えます。

サンプルコード

サンプルコードでは、UIViewControllerのviewDidLoadviewWillAppearviewDidAppearviewWillDisappearviewDidDisappearを、NSLog出力付きの自作メソッドに入れ替えています。

まずは、UIViewControllerをカテゴリ拡張して、入れ替えるメソッドの実装と、method_exchangeImplementations()を呼び出すクラスメソッドの実装を行います。

UIViewController+MethodSwitch.h
#import <UIKit/UIKit.h>
@interface UIViewController(MethodSwitch)
+(void)switchLoggingMethod;
@end
UIViewController+MethodSwitch.m
#import "UIViewController+MethodSwitch.h"
#import <objc/runtime.h>

@implementation UIViewController(MethodSwitch)

// 入れ替えるメソッドを準備
-(void)loggingViewDidLoad
{
    NSLog(@"%@ viewDidLoad",NSStringFromClass(self.class));
    [self loggingViewDidLoad];  // ここ、無限ループしそうだけど、メソッドの実装が入れ替わるので、元々あったviewDidLoadの実装が呼ばれます。
}
-(void)loggingViewWillAppear:(BOOL)animated{
    NSLog(@"%@ viewWillAppear",NSStringFromClass(self.class));
    [self loggingViewWillAppear:animated];
}
-(void)loggingViewDidAppear:(BOOL)animated{
    NSLog(@"%@ viewDidAppear",NSStringFromClass(self.class));
    [self loggingViewDidAppear:animated];
}
-(void)loggingViewWillDisappear:(BOOL)animated{
    NSLog(@"%@ viewWillDisappear",NSStringFromClass(self.class));
    [self loggingViewWillDisappear:animated];
}
-(void)loggingViewDidDisappear:(BOOL)animated{
    NSLog(@"%@ viewDidDisappear",NSStringFromClass(self.class));
    [self loggingViewDidDisappear:animated];
}

+(void)switchLoggingMethod
{
    // メソッドを入れ替える
    [self switchInstanceMethodFrom:@selector(viewDidLoad)        To:@selector(loggingViewDidLoad)       ];
    [self switchInstanceMethodFrom:@selector(viewWillAppear:)    To:@selector(loggingViewWillAppear:)   ];
    [self switchInstanceMethodFrom:@selector(viewDidAppear:)     To:@selector(loggingViewDidAppear:)    ];
    [self switchInstanceMethodFrom:@selector(viewWillDisappear:) To:@selector(loggingViewWillDisappear:)];
    [self switchInstanceMethodFrom:@selector(viewDidDisappear:)  To:@selector(loggingViewDidDisappear:) ];
}

+(void)switchInstanceMethodFrom:(SEL)from To:(SEL)to
{
    // メソッドの入れ替えの実態はここ
    Method fromMethod = class_getInstanceMethod(self,from);
    Method toMethod   = class_getInstanceMethod(self,to  );
    method_exchangeImplementations(fromMethod, toMethod);
}
@end

また、AppDelegate内で、[UIViewController switchLoggingMethod]を呼び出します。

AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    [UIViewController switchLoggingMethod];  // ←これでメソッドが入れ替わる
    return YES;
}

実行するとデバッグエリアにログが出力されます。
MethodSwitch_xcodeproj_—_AppDelegate_m.png

UIViewControllerそのものでも有効になりますし、それを継承したクラス(UINavigationController等)でも有効になります。

用量用法を守って、正しくお使いください。

このメソッドの入れ替えですが、普通にコードを読んで入れ替わってる事に気づくのは難しいです。知らない人がソースコードを読むと、「viewDidLoadに何も実装していないのにログが出力されるナンデ?!」と大混乱する事必至です。チームメンバーや未来の自分を混乱させないように、この辺りの情報はがっつり残しておきましょう。

でも、悪戯って楽しいよね。

例えば NSObject のメソッドを入れ替えちゃったりして。うへへへ

参考にしたサイト

Safari用独自プラグインを作る(4) - Method Swizzling を試す
method_exchangeImplementationsでクラスメソッドを置換する
☆ モテる Objective-C 女子力を磨くための4つの心得「method swizzling できない女をアピールせよ」等 ←タイトルに負けた